Maker.io main logo

Prop It Game

62

2026-08-18 | By Adafruit Industries

License: See Original Project 3D Printing Adafruit Feather

Courtesy of Adafruit

Guide by Ruiz Brothers and Liz Clark

Overview

Bop It Inspired Game

Build and play a modern version of the iconic 90's hit game, Bop It. Powered by the Feather RP2040 PropMaker running Circuit Python. A 14-segment alphanumeric display is used to show which input to hit and keeps track of your score. Music and sound effects play through the mini speaker while a NeoPixel stick visualizes the turns countdown. All of the electronics are housed in a 3D printed retro-future inspired enclosure.

Build it, Play it!

The goal of the game is to hit the correct input control before the time runs out! The game features Push it, Slide it, Flip it, Shake it and Spin it! Using a key switch, slide potentiometer, toggle switch, accelerometer, and hall effect sensor.

Each turn is randomized, making it challenging. As the game advances, the music speeds up and the time to strike increases making each turn more difficult as you play.

game_1

Portable Handheld Game

The device houses a rechargeable 2200mAh cylindrical battery for several hours of game play. The enclosure features a funky design that includes a handle for extra grip support. The spin wheel incorporates a ball bearing with embedded neodymium magnets for triggering the hidden hall effect sensor. Each input control feels good when actuated and comes alive with the custom made sound effects.

handheld_2

star_3

Pick your own PLA filaments to make a custom color combo that’s fits for your personal taste. Whether you're building one for yourself, or a friend/family, we hope building it as just as fun as playing it!

Enclosure designed and project idea by Noe Ruiz. Music and sound effects by Dan Q. Code by Liz Clark. Graphic design by Bruce Yan.

Parts

Hardware Fasteners

These screws are required for assembling this project.

  • 3x M3 x 6mm steel machine screws

  • 4x M3 x 4mm steel machine screws

  • 1x M3 x 20mm steel machine screws

  • 2x M2.5 x 4mm steel machine screws

  • 2x M2 x 6mm steel machine screws

  • 2x M2 x 8mm steel machine screws

Circuit Diagram

The diagram below provides a general visual reference for wiring of the components once you get to the Assembly page. This diagram was created using the software package Fritzing.

Adafruit Library for Fritzing

Adafruit uses the Adafruit Fritzing parts library to create circuit diagrams for projects. You can download the library or just grab individual parts. Get the library and parts from GitHub - Adafruit Fritzing Parts.

parts_4

Wired Connections

The PropMaker Feather RP2040 is powered by a 3.7v 2200mAh lithium ion polymer battery. Wires connecting various components:

  • VCC(+) and GND(–) from Speaker to VO+ and VO– pins on Feather

  • DIN from NeoPixel stick to NeoPixel Input on Feather

  • 5V from NeoPixel stick to 5V on Feather

  • GND on NeoPixel stick to GND on Feather

  • STEMMA JST connector from LED backpack connects to STEMMA JST connector on Feather

  • GND from Feather to Pin 1 on Slide Switch

  • EN from Feather to Pin 2 on Slide Switch

  • VCC from hall effect sensor to V+ on Feather

  • GND from hall effect sensor to GND on Feather

  • Signal from hall effect sensor to Sig on Feather

  • 10K resistor in between VCC and Signal pins on hall effect sensor

  • D5 from Feather to Pin 1 on Push Button

  • A0 from Feather to Pin 2 on Slide Potentiometer

  • Pin 1 (VCC) from Slide Potentiometer to 3.3V pin on Feather

  • External Button from Feather to Pin 1 on Toggle Switch

  • Pin 2 from Toggle Switch to GND on Feather

  • Pin 3 (GND) from Slide Potentiometer to Pin 2 on Push Button and Pin 2 on Toggle Switch

CAD Files

CAD Parts

Individual 3MF files for 3D printing are oriented and ready to print on FDM machines using PLA filament. Original design source files may be downloaded using the links below.

cad_5

Download 3MF Files

Dowload CAD Source

Build Volume

The parts require a 3D printer with a minimum build volume of 100 (X) x 135 (Y) x 31mm (Z).

build_6

Multi-color Part

The front half of the enclosure uses an AMS multi-color capable 3D printer. A single color version is available, along with an LED diffuser piece. The LED diffuser is recommended to be printed using a white or clear colored PLA filament.

enclosure_7

3D Models of Adafruit Parts

Electronic components like Adafruit's boards and more can be downloaded from the Adafruit CAD parts GitHub Repo.

component_8

Install CircuitPython

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython QuickStart

Follow this step-by-step to quickly get CircuitPython running on your board.

Download the latest version of CircuitPython for this board via circuitpython.org

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

download_9

board_10

To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in red or blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!

If the drive does not appear, release all the buttons, and then repeat the process above.

You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RPI-RP2.

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.

drag_11

drag_12

The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

drive_13

Safe Mode

You want to edit your code.py or modify the files on your CIRCUITPY drive, but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.

Safe mode in CircuitPython does not run any user code on startup, and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.

Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.

Entering Safe Mode

To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

In Safe Mode

If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.

If you connect to the serial console, you'll find the following message.

Copy Code
Auto-reload is off.
Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.

You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.

Flash Resetting UF2

If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RPI-RP2. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.

Download flash erasing "nuke" UF2

Code the Game

Once you've finished setting up your RP2040 Prop-Maker Feather with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.

To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2026 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT
'''Prop-It CircuitPython code. Feather Prop Maker RP2040 with MX key, hall effect sensor,
toggle switch, slide potentiometer and onboard accelerometer. Background music plays in loop
and individual sound effects for each input play with audio mixer'''

import random
import board
import neopixel
import audiobusio
import audiomixer
import audiocore
import audiospeed
import adafruit_lis3dh
from analogio import AnalogIn
from digitalio import DigitalInOut, Direction, Pull
from adafruit_ht16k33 import segments
from rainbowio import colorwheel
import simpleio
from adafruit_ticks import ticks_ms, ticks_diff, ticks_add

# external power
external_power           = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value     = True

# alphanumeric display
display = segments.Seg14x4(board.STEMMA_I2C())

# neopixel strip
pixels = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, 8, brightness=0.6, auto_write=True)

# accelerometer
i2c = board.I2C()
int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
SHAKE_THRESHOLD = 15

key = DigitalInOut(board.D5) # mx key
key.direction = Direction.INPUT
key.pull = Pull.UP
toggle = DigitalInOut(board.EXTERNAL_BUTTON) # toggle switch
toggle.direction = Direction.INPUT
toggle.pull = Pull.UP
hall = DigitalInOut(board.EXTERNAL_SERVO) # hall effect sensor
hall.direction = Direction.INPUT
hall.pull = Pull.UP

# potentiometer
slider = AnalogIn(board.A0)
last_slider_zone = [0]

def get_slider_zone(): # helper for slider to read it as a zone
    raw = slider.value
    zone = int(simpleio.map_range(raw, 0, 65535, 0, 4))
    if abs(zone - last_slider_zone[0]) >= 2:
        last_slider_zone[0] = zone
    return last_slider_zone[0]

TEXT_FILES = ["/sfx_files/push.wav", "/sfx_files/flip.wav", "/sfx_files/spin.wav",
              "/sfx_files/slide.wav", "/sfx_files/shake.wav"]
SFX_FILES = ["/sfx_files/push_it_fx.wav", "/sfx_files/flip_it_fx.wav", "/sfx_files/spin_it_fx.wav",
             "/sfx_files/slide_it_fx.wav", "/sfx_files/shake_it_fx.wav"]
inputs = [
    {'label': "PUSH", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[0],
     'sfx_file': SFX_FILES[0], 'check': lambda: key.value},
    {'label': "FLIP", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[1],
     'sfx_file': SFX_FILES[1], 'check': lambda: toggle.value},
    {'label': "SPIN", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[2],
     'sfx_file': SFX_FILES[2], 'check': lambda: hall.value},
    {'label': "SLDE", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[3],
     'sfx_file': SFX_FILES[3], 'check': get_slider_zone},
    {'label': "SHKE", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[4],
     'sfx_file': SFX_FILES[4], 'check': lambda: lis3dh.shake(shake_threshold=SHAKE_THRESHOLD)},
]

# audio mixer setup - import wav files

try:
    bg_file = open("/sfx_files/Beat.wav", "rb")
    wav = audiocore.WaveFile(bg_file)
    wav_bg = audiospeed.SpeedChanger(wav, rate=1.0)
except OSError as e:
    print(f"Missing Beat.wav: {e}")

try:
    txt_file = open(TEXT_FILES[0], "rb")
    wav_sfx  = audiocore.WaveFile(txt_file)
except OSError as e:
    print(f"Missing {TEXT_FILES[0]}: {e}")
SAMPLE_RATE = 22050
i2s   = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(
    voice_count=2,
    sample_rate=SAMPLE_RATE,
    channel_count=1,
    bits_per_sample=16,
    buffer_size=2048
)
i2s.play(mixer)
mixer.voice[0].level = 0.4
mixer.voice[0].play(wav_bg, loop=True) # background music loops
mixer.voice[1].level = 0.5

def play_sfx(file): # helper to play sfx
    sfx = open(file, "rb")
    w = audiocore.WaveFile(sfx)
    mixer.voice[1].play(w, loop=False)

def scroll_text(txt, scroll_x, count = 0, counting = False):
    padded = "    " + txt + "    "
    display.print(padded[scroll_x:scroll_x + 4])
    display.show()
    scroll_x = (scroll_x + 1) % (4 + len(txt))
    if scroll_x == 0:
        count += 1
    if counting:
        return scroll_x, count
    else:
        return scroll_x

state_changed = False # check for correct input
wrong_input = False # check for wrong input
seed = 0 # random seed
hue = 0 # neopixel hue
game_start = True # first run
TRIGGER_INTERVAL = 2500 # time between seeds
TRIGGER_LIMIT = 1000 # shortest time between seeds
STATE_CHANGE_TIMEOUT = 2000 # time for inputs
STATE_CHANGE_LIMIT = 1000 # shortest time for inputs
INTERVAL_CHANGE = 150 # amount of time subtracted every 5 turns
speed = 0.5 # music speed
wav_bg.rate = speed
score = 0 # game score
scroll_x_pos = 0 # text scroll position
scroll_count = 0 # text scroll count
timer = ticks_ms()
IGNORE_MAP = { # don't check for spin during shake, vice versa
    4: [2],
    2: [4],
}

for i in range(len(inputs)): # reset all input states before game start
    inputs[i]['last_state'] = inputs[i]['check']()
    inputs[i]['current_state'] = False

while True:
    if game_start: # scroll start text, wait for button input to start
        if ticks_diff(ticks_ms(), timer) >= 250:
            scroll_x_pos = scroll_text("START?", scroll_x_pos)
            hue = (hue + 2) % 256
            pixels.fill(colorwheel(hue))
            timer = ticks_add(timer, 250)
        if not inputs[0]['check']() and not inputs[0]['current_state']:
            inputs[0]['current_state'] = True
        if inputs[0]['check']() and inputs[0]['current_state']:
            inputs[0]['current_state'] = False
            play_sfx("/sfx_files/game_start.wav")
            scroll_x_pos = 0
            display.print("****")
            display.show()
            timer = ticks_ms()
            game_start = False  # go into game
    else:
        if ticks_diff(ticks_ms(), timer) >= TRIGGER_INTERVAL:
            pixels.fill((0, 0, 0))
            for inp in inputs: # reset states and read all pins
                inp['last_state'] = inp['check']()
                inp['current_state'] = False
            seed = random.randint(0, 4) # pick seed
            play_sfx(inputs[seed]['txt_file']) # play audio clip
            display.print(inputs[seed]['label']) # update display
            display.show()
            secondary_timer = ticks_ms()
            # while the game timer is running..
            while ticks_diff(ticks_ms(), secondary_timer) < STATE_CHANGE_TIMEOUT:
                time_left = ticks_diff(ticks_ms(), secondary_timer) # countdown pixels with timer
                num_pixels = simpleio.map_range(time_left, 0, STATE_CHANGE_TIMEOUT, 0, 7)
                for i in range(num_pixels):
                    pixels[i] = ((255, 0, 255))
                # check for state change
                if inputs[seed]['check']() != inputs[seed]['last_state']:
                    pixels.fill((0, 255, 0))
                    state_changed = True
                    break
                if state_changed:
                    break
                # check wrong inputs
                for i, inp in enumerate(inputs):
                    if i == seed:
                        continue
                    if i in IGNORE_MAP.get(seed, []):
                        # don't check for spin during shake, vice versa
                        continue
                    if inp['check']() != inp['last_state']: # wrong input, game over
                        pixels.fill((255, 0, 0))
                        wrong_input = True
                        break
                if wrong_input:
                    break

            if state_changed: # you scored!
                play_sfx(inputs[seed]['sfx_file'])
                score += 1
                display.print(f"{score:>4}")
                display.show()
                if score % 5 == 0: # every 5 turns, increase speed
                    TRIGGER_INTERVAL -= INTERVAL_CHANGE
                    TRIGGER_INTERVAL = max(TRIGGER_INTERVAL, TRIGGER_LIMIT)
                    STATE_CHANGE_TIMEOUT -= INTERVAL_CHANGE
                    STATE_CHANGE_TIMEOUT = max(STATE_CHANGE_TIMEOUT, STATE_CHANGE_LIMIT)
                    speed = simpleio.map_range(
                                     STATE_CHANGE_TIMEOUT, STATE_CHANGE_LIMIT, 2000, 2.0, 0.5)
                    wav_bg.rate = speed # music speeds up too
                state_changed = False # reset state and clock
                timer = ticks_ms()
            else:
                timer = ticks_ms()
                speed = 0.5 # reset music speed
                wav_bg.rate = speed
                play_sfx("/sfx_files/game_over.wav")
                pixels.fill((255, 0, 0))
                while scroll_count < 2: # scroll game over and score text 2x
                    if ticks_diff(ticks_ms(), timer) >= 250:
                        scroll_x_pos, scroll_count = scroll_text(
                                                     f"GAME OVER - SCORE: {score}", scroll_x_pos,
                                                     scroll_count, counting = True)
                        timer = ticks_add(timer, 250)
                scroll_count = 0 # reset all the states to restart game
                scroll_x_pos = 0
                score = 0
                TRIGGER_INTERVAL = 2500
                STATE_CHANGE_TIMEOUT = 2000
                wrong_input = False
                game_start = True
                timer = ticks_ms()

View on GitHub

Upload the Code and Libraries to the RP2040 Prop-Maker Feather

After downloading the Project Bundle, plug your RP2040 Prop-Maker Feather into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the RP2040 Prop-Maker Feather's CIRCUITPY drive.

  • lib folder

  • sfx_files folder

  • code.py

Your RP2040 Prop-Maker Feather CIRCUITPY drive should look like this after copying the lib folder, sfx_files folder, and code.py file:

folder_14

How the CircuitPython Code Works

The code begins by initializing all of the external components: the alphanumeric display, NeoPixel stick, onboard accelerometer, MX key, toggle switch, hall effect sensor and slide potentiometer.

A helper function maps the analog readings from the potentiometer to zones that are then checked for change during gameplay. This keeps the readings accurate.

Download File

Copy Code
# external power
external_power           = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value     = True

# alphanumeric display
display = segments.Seg14x4(board.STEMMA_I2C())

# neopixel strip
pixels = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, 8, brightness=0.6, auto_write=True)

# accelerometer
i2c = board.I2C()
int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
SHAKE_THRESHOLD = 15

key = DigitalInOut(board.D5) # mx key
key.direction = Direction.INPUT
key.pull = Pull.UP
toggle = DigitalInOut(board.EXTERNAL_BUTTON) # toggle switch
toggle.direction = Direction.INPUT
toggle.pull = Pull.UP
hall = DigitalInOut(board.EXTERNAL_SERVO) # hall effect sensor
hall.direction = Direction.INPUT
hall.pull = Pull.UP

# potentiometer
slider = AnalogIn(board.A0)
last_slider_zone = [0]

def get_slider_zone(): # helper for slider to read it as a zone
    raw = slider.value
    zone = int(simpleio.map_range(raw, 0, 65535, 0, 4))
    if abs(zone - last_slider_zone[0]) >= 2:
        last_slider_zone[0] = zone
    return last_slider_zone[0]

Dictionary

A dictionary called inputs tracks the states of the components as well as their labels for the alphanumeric display, sound effects and function calls to read their values.

Download File

Copy Code
TEXT_FILES = ["/sfx_files/push.wav", "/sfx_files/flip.wav", "/sfx_files/spin.wav",
              "/sfx_files/slide.wav", "/sfx_files/shake.wav"]
SFX_FILES = ["/sfx_files/push_it_fx.wav", "/sfx_files/flip_it_fx.wav", "/sfx_files/spin_it_fx.wav",
             "/sfx_files/slide_it_fx.wav", "/sfx_files/shake_it_fx.wav"]
inputs = [
    {'label': "PUSH", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[0],
     'sfx_file': SFX_FILES[0], 'check': lambda: key.value},
    {'label': "FLIP", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[1],
     'sfx_file': SFX_FILES[1], 'check': lambda: toggle.value},
    {'label': "SPIN", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[2],
     'sfx_file': SFX_FILES[2], 'check': lambda: hall.value},
    {'label': "SLDE", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[3],
     'sfx_file': SFX_FILES[3], 'check': get_slider_zone},
    {'label': "SHKE", 'current_state': False, 'last_state': False, 'txt_file': TEXT_FILES[4],
     'sfx_file': SFX_FILES[4], 'check': lambda: lis3dh.shake(shake_threshold=SHAKE_THRESHOLD)},
]

Mixing Audio

A Mixer is used to play the background audio track in a loop with the sound effects. The speed of the background track changes depending on how fast the game is moving with SpeedChanger.

Download File

Copy Code
# audio mixer setup - import wav files

try:
    bg_file = open("/Beat.wav", "rb")
    wav = audiocore.WaveFile(bg_file)
    wav_bg = audiospeed.SpeedChanger(wav, rate=1.0)
except OSError as e:
    print(f"Missing Beat.wav: {e}")

try:
    sfx_file = open(SFX_FILES[0], "rb")
    wav_sfx  = audiocore.WaveFile(sfx_file)
except OSError as e:
    print(f"Missing {SFX_FILES[0]}: {e}")
SAMPLE_RATE = 22050
i2s   = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(
    voice_count=2,
    sample_rate=SAMPLE_RATE,
    channel_count=1,
    bits_per_sample=16,
    buffer_size=2048
)
i2s.play(mixer)
mixer.voice[0].level = 0.4
mixer.voice[0].play(wav_bg, loop=True) # background music loops
mixer.voice[1].level = 0.5

def play_sfx(sfx_key): # helper to play sfx
    sfx = open(inputs[sfx_key]['sfx_file'], "rb")
    w = audiocore.WaveFile(sfx)
    mixer.voice[1].play(w, loop=False)

Non-Blocking Scrolling

The scroll_text() helper scrolls text on the alphanumeric display. It tracks the x position of the text with empty spaces and writes to the display.

Download File

Copy Code
def scroll_text(txt, scroll_x, count = 0, counting = False):
    padded = "    " + txt + "    "
    display.print(padded[scroll_x:scroll_x + 4])
    display.show()
    scroll_x = (scroll_x + 1) % (4 + len(txt))
    if scroll_x == 0:
        count += 1
    if counting:
        return scroll_x, count
    else:
        return scroll_x

A few states and variables are defined before the loop.

Download File

Copy Code
state_changed = False # check for correct input
wrong_input = False # check for wrong input
seed = 0 # random seed
hue = 0 # neopixel hue
game_start = True # first run
TRIGGER_INTERVAL = 2500 # time between seeds
TRIGGER_LIMIT = 1000 # shortest time between seeds
STATE_CHANGE_TIMEOUT = 2000 # time for inputs
STATE_CHANGE_LIMIT = 1000 # shortest time for inputs
INTERVAL_CHANGE = 150 # amount of time subtracted every 5 turns
speed = 0.5 # music speed
wav_bg.rate = speed
score = 0 # game score
scroll_x_pos = 0 # text scroll position
scroll_count = 0 # text scroll count
timer = ticks_ms()
IGNORE_MAP = { # don't check for spin during shake, vice versa
    4: [2],
    2: [4],
}

The Loop

During the first run or in between games, the text "START?" scrolls across the alphanumeric display. When you press the MX key, it triggers a game to start.

Download File

Copy Code
while True:
    if game_start: # scroll start text, wait for button input to start
        if ticks_diff(ticks_ms(), timer) >= 250:
            scroll_x_pos = scroll_text("START?", scroll_x_pos)
            hue = (hue + 2) % 256
            pixels.fill(colorwheel(hue))
            timer = ticks_add(timer, 250)
        if not inputs[0]['check']() and not inputs[0]['current_state']:
            inputs[0]['current_state'] = True
        if inputs[0]['check']() and inputs[0]['current_state']:
            inputs[0]['current_state'] = False
            play_sfx("/sfx_files/game_start.wav")
            scroll_x_pos = 0
            display.print("****")
            display.show()
            timer = ticks_ms()
            game_start = False  # go into game

Gameplay

Gameplay is handled with two timers. The first timer handles triggering a new turn. All of the states are reset to the current reading from all of the inputs to avoid any false reads. A random integer, seed, determines which input will be required for the turn. The matching sound effect plays and the display shows the input name. Then, the second timer starts.

Download File

Copy Code
else:
        if ticks_diff(ticks_ms(), timer) >= TRIGGER_INTERVAL:
            pixels.fill((0, 0, 0))
            for inp in inputs: # reset states and read all pins
                inp['last_state'] = inp['check']()
                inp['current_state'] = False
            seed = random.randint(0, 4) # pick seed
            play_sfx(seed) # play audio clip
            display.print(inputs[seed]['label']) # update display
            display.show()
            secondary_timer = ticks_ms()

The second timer is compared against the value of STATE_CHANGE_TIMEOUT to create a countdown timer. The NeoPixel stick acts as a visualizer for the timer. The time_left is mapped from the time limit to the 8 NeoPixels. As the turn runs out of time, you'll see the NeoPixels light up.

The input at the seed index value is checked against the previously known state to check for a change. If a change is detected, then state_changed is set to True, which will result in scoring a point. If a change is not detected or an input at a different index value detects a change, wrong_input is set to True. This triggers an end of game.

While testing, spinning the ball bearing for the hall effect sensor would trigger the accelerometer shake the detection and shaking would trigger the hall effect sensor. Because of that, these inputs are not checked against each other if one of them is the chosen seed.

Download File

Copy Code
# while the game timer is running..
            while ticks_diff(ticks_ms(), secondary_timer) < STATE_CHANGE_TIMEOUT:
                time_left = ticks_diff(ticks_ms(), secondary_timer) # countdown pixels with timer
                num_pixels = simpleio.map_range(time_left, 0, STATE_CHANGE_TIMEOUT, 0, 7)
                for i in range(num_pixels):
                    pixels[i] = ((255, 0, 255))
                # check for state change
                if inputs[seed]['check']() != inputs[seed]['last_state']:
                    pixels.fill((0, 255, 0))
                    state_changed = True
                    break
                if state_changed:
                    break
                # check wrong inputs
                for i, inp in enumerate(inputs):
                    if i == seed:
                        continue
                    if i in IGNORE_MAP.get(seed, []):
                        # don't check for spin during shake, vice versa
                        continue
                    if inp['check']() != inp['last_state']: # wrong input, game over
                        pixels.fill((255, 0, 0))
                        wrong_input = True
                        break
                if wrong_input:
                    break

Winning and Losing

If you score, the value of score increases by 1 and it is shown on the display. Every five turns, the speed of the game increases. The speed of the background music also increases. There are ceiling limits for the speed of the game, held in TRIGGER_LIMIT and STATE_CHANGE_LIMIT.

Download File

Copy Code
if state_changed: # you scored!
                play_sfx(inputs[seed]['sfx_file'])
                score += 1
                display.print(f"{score:>4}")
                display.show()
                if score % 5 == 0: # every 5 turns, increase speed
                    TRIGGER_INTERVAL -= INTERVAL_CHANGE
                    TRIGGER_INTERVAL = max(TRIGGER_INTERVAL, TRIGGER_LIMIT)
                    STATE_CHANGE_TIMEOUT -= INTERVAL_CHANGE
                    STATE_CHANGE_TIMEOUT = max(STATE_CHANGE_TIMEOUT, STATE_CHANGE_LIMIT)
                    speed = simpleio.map_range(
                                     STATE_CHANGE_TIMEOUT, STATE_CHANGE_LIMIT, 2000, 2.0, 0.5)
                    wav_bg.rate = speed # music speeds up too
                state_changed = False # reset state and clock
                timer = ticks_ms()

If you don't score a point, the game ends. The background music goes back to the initial half speed and the text "GAME OVER - SCORE: #" scrolls across the display twice to show your final score. All of the variables are reset, including the timer speeds, and you are brought back to the initial state at the top of the loop asking if you want to start a new game.

Download File

Copy Code
else:
                timer = ticks_ms()
                speed = 0.5 # reset music speed
                wav_bg.rate = speed
                play_sfx("/sfx_files/game_over.wav")
                pixels.fill((255, 0, 0))
                while scroll_count < 2: # scroll game over and score text 2x
                    if ticks_diff(ticks_ms(), timer) >= 250:
                        scroll_x_pos, scroll_count = scroll_text(
                                                     f"GAME OVER - SCORE: {score}", scroll_x_pos,
                                                     scroll_count, counting = True)
                        timer = ticks_add(timer, 250)
                scroll_count = 0 # reset all the states to restart game
                scroll_x_pos = 0
                score = 0
                TRIGGER_INTERVAL = 2500
                STATE_CHANGE_TIMEOUT = 2000
                wrong_input = False
                game_start = True
                timer = ticks_ms()

Wiring

Slide Switch Wires

Prepare a set of two wires about 3 inches (7cm) in length.

Using wire strippers, remove a bit of insulation from the tips of each wire.

Tin the exposed wire by applying a bit of solder to them.

Snip off one of the leads on the slide switch, either the far left or right but not the middle. Then trim the two remaining leads short, about half their length.

wiring_15

wiring_16

Solder Wires

Solder the two wires to each of the leads on the slide switch.

A pair of helping hands can assist while soldering wires in place.

Solder the two wires from the slide to the EN and GND pins on the top of the Feather.

solder_17

solder_18

solder_19

Hall Effect Sensor Prep

Prepare a set of three multi-colored wires (red, blue and white) about 5 inches (12cm) in length.

Trim the legs short from the 10K resistor and hall effect sensor.

A piece of heat-shrink tubing can help keep the wires bundled together.

prep_20

prep_21

Solder Hall Effect Sensor to Feather

With sensor facing up, solder Pin 1 to the red wire and a leg of the 10K resistor.

Solder the blue wire to Pin 2 of the hall effect sensor.

Solder white wire to Pin 3 and the other leg of the 10K resistor.

With Feather facing down, solder red wire to 5V pin, blue wire to GND pin and white to Servo pin.

solder_22

solder_23

Wires from NeoPixel Stick

Prepare a set of three wires about 3.5 inches (8cm) in length.

Solder the three wires to the GND, DIN, and 5VDC pads on the back of the NeoPixel stick.

wires_24

wires_25

Shared Ground Wires

Prepare ground wires for sharing across the slide potentiometer, key switch and toggle switch.

  • Short wire: 1 inch (2.5cm)

  • Medium Wire: 2 inches (5m)

  • Long Wire: 4.5 inches (11.5cm)

ground_26

Panel Mount Controls

Get the three input controls ready to panel mount to the front half of the enclosure.

Remove the hardware from the toggle switch.

Insert the toggle switch through the appropriately sized hole.

Use the hex nut to secure the toggle switch to the enclosure.

panel_27

panel_28

panel_29

Panel Mount Slider

Place the slide potentiometer onto the front half of the enclosure with the nub fitting through the slit.

While holding slide potentiometer, turn the enclosure over, line up the two mounting holes.

Insert and fasten two M2 x 8mm long machine screws to secure the slide potentiometer to the enclosure.

slider_30

slider_31

Mount Key Switch

Orient the choc key switch and press fit it into the square cutout on the front half of the enclosure.

mount_32

mount_33

mount_34

Solder Ground Wires

Start with soldering the medium ground wire to the toggle switch.

Solder the other end of the medium wire and the short wire to Pin 3 on the slide potentiometer.

Solder the other end of the short wire and the long wire to either pin on the choc key switch.

ground_35

Input Control Signal Wires

Prepare three multi-color wires (white, green, yellow and red) about 4.5 inches (11.5cm) for the input controls.

input_36

Solder Wires to Slide Pot

Solder the red wire to pin 1 on the slide pot.

Solder the yellow wire to pin 2 on the slide pot.

pot_37

Solder Wire to Toggle

Solder the white wire to the either far left or right pin on the toggle switch.

toggle_38

Solder Wire to Key Switch

Solder the green wire to the remaining pin on the choc key switch.

switch_39

Soldered Wires

Take a moment to check all of the wires have been properly soldered to the various pins.

Adding a piece of heat-shrink tubing can help keep the various wires bundled together.

adding_40

Feather Pre-solder

Get the Feather ready to solder the signal wires.

feather_41

Solder Wires to Feather

Solder the green wire (choc key switch) to pin 5.

Solder the red wire (VCC on slide pot) to one of the 3.3V pins.

Solder the yellow wire (wiper on slide pot) to the A0 pin.

wires_42

Secure Toggle Switch Wire

Insert the white wire from the toggle switch to the Btn labeled pin on the Feathers screw block terminal.

Use a flat head screwdriver to secure the wire to the screw block terminal.

secure_43

secure_44

NeoPixel to Feather

Get the NeoPixel stick ready to connect to the Feather.

neopixel_45

Secure Ground Wires to Feather

Locate the ground wire from the NeoPixel stick and the remaining ground wire from the choc key switch and insert them both into the ground labeled pin on the Feathers screw block terminal.

Use a flat head screwdriver to secure the two ground wires to the screw block terminal.

locate_46

locate_47

Secure NeoPixel Wires

Insert the VCC wire from the NeoPixel stick to the 5V labeled pin on the Feather screw block terminal.

Insert the DIN wire from the NeoPixel stick to the Neo labeled pin on the Feather screw block terminal.

Use a flat head screwdriver to secure the two ground wires to the screw block terminal.

insert_48

insert_49

Prep Speaker Wires

Shorten the socket picoBlade Molex cable so it’s about 2.5 inches (6.3cm) in length.

Using wire strippers, expose about 0.12in (3mm) of each wire.

Connect it to the speakers plug cable.

speaker_50

speaker_55

Secure Speaker Wire to Feather

Insert the wires from the speaker to the audio pins on the Feather screw block terminal (red wire to positive, black wire to negative).

Use the screwdriver to secure the wires to the screw block terminal.

insert_51

insert_52

Check Wired Connections

Take a moment to inspect your work, making sure all of the wires have been properly soldered and secured.

check_52

Assembly

Secure Display Back

Orient the display back cover with the back half of the enclosure – The bigger rounded corners should face up.

Use two M3 x 6mm long machine screws to secure the display back cover to the back half of the enclosure.

Place the display back cover over the standoffs with the mounting holes lined up.

Insert and fasten the screws to secure the two parts together.

display_53

display_54

display_55

Install LED Display Backpack

Connect the STEMMA QT cable to the STEMMA QT port on the right-side of the LED backpack PCB.

Insert and pull the STEMMA QT cable from the LED display through the left-side of the back cover.

Fit the PCB under the bottom clips on the back cover.

Press the PCB down to fit under the remaining clips.

backpack_56

backpack_57

backpack_58

backpack_59

backpack_60

Secure Feather to Case

Orient the Feather with the back half of the enclosure.

Use two M2.5 x 4mm long machine screws to secure the Feather.

Place the Feather over the standoffs on the enclosure.

Insert and fasten the screws to secure the Feather to the case.

case_61

case_62

case_63

Secure Slide Switch

Insert the slide switch to the holder built into the back half of the enclosure.

slide_64

Connect and Install Battery to Case

Orient the 2200mAh battery with the case.

Connect the cable from the battery to the Feather.

Press the battery into the clips to snap it into the back half of the enclosure.

connect_65

connect_66

connect_67

Secure NeoPixel Stick to Case

Orient the NeoPixel stick with the back half of the case.

Place the NeoPixel stick over the standoffs with the mounting holes lined up.

Use two M2 x 6mm long machine screws to secure the parts together.

stick_68

stick_69

Connect STEMMA from LED Backpack

Connect the STEMMA QT cable from the LED display to the STEMMA QT port on the Feather.

stemma_70

Secure Speaker to Case

Place the speaker over the speaker holder on the back half of the case with it facing up.

speaker_71

Wheel Parts

Get the various wheel parts ready to assemble together.

  • 1x M3 x 6mm long machine screw

  • 4x neodymium magnets

  • 1x ball bearing 608ZZ

  • 1x wheel-bore

  • 1x wheel-front

  • 1x wheel-back

  • 1x wheel

wheel_72

Test Magnet Polarities

The magnets must have their south pole facing outwards, with the north pole sticking to the outer diameter of the ball bearing.

Place the four neodymium magnets on the ball bearing, as shown in the photo.

Test the polarities are correct using the hall effect sensor and demo code.

test_72

Hall Effect Sensor Demo Code

Download the code.py file below. On the Feather CIRCUITPY drive, rename the game code file to "game-code.py". Then, upload the hall effect demo code to the drive.

Whenever the hall effect sensor detects the south pole of a magnet, the NeoPixels will turn bright red. If it doesn’t, simply flip the magnet over.

When all four magnets are tested and trigger the NeoPixels, delete the hall effect sensor demo code from the CIRCUITPY drive and rename the game code back to code.py. Then, proceed with the assembly.

hall_73

Download code.py

Copy Code
import time
import random
import board
import neopixel
from digitalio import DigitalInOut, Direction, Pull

# external power
external_power           = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value     = True

# neopixel strip
pixels = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, 8, brightness=0.6, auto_write=True)

# hall effect sensor
hall = DigitalInOut(board.EXTERNAL_SERVO)
hall.direction = Direction.INPUT
hall.pull = Pull.UP

while True:
    if hall.value:
        pixels.fill((255, 0, 0))
    else:
        pixels.fill((0,0,0))
    time.sleep(0.01)

Bearing Wheel Install

Insert the ball bearing with the neodymium magnets into the wheel with the cutouts matching.

bearing_74

bearing_75

Assemble Wheel Case

Place the ball bearing onto the wheel back cover.

Insert the wheel bore through the center hole on the wheel front cover.

Insert the wheel front cover with wheel bore through the center of the ball bearing.

Insert and fasten the M3 x 6mm long machine screw to secure the parts together.

The wheel should be free-spinning.

assemble_76

assemble_77

assemble_78

assemble_79

assemble_80

Secure Hall Effect Sensor

Orient the hall effect sensor with the wheel so it can detect the four neodymium magnets (side with the markings should face the wheel with the embedded magnets).

Push the hall effect sensor into the square cutout in the wheel holder assembly.

sensor_81

sensor_82

Install Wheel Assembly

Orient the wheel assembly with the back half of the enclosure.

The registration nub should fit through the matching notch on the wheel assembly.

orient_83

LED Display Parts

Get the parts to build the LED display enclosure ready.

An optional gel filter is cut to size to fit inside the LED display case.

led_84

Assemble LED Display Case

Orient the LED display face plate with the LED display frame (large, rounded corners matching).

Push the face plate into the frame so it sits flush with the internal lip.

Place the optional gel filter over the face plate, inside the LED display frame.

filter_85

filter_86

filter_87

Install LED Display Case

Orient the LED display case with the LED display backpack.

Press the LED display frame over the LED display back cover to snap fit them together.

install_88

install_89

Case Check

Take a moment to spect all of the parts are fitting together correctly.

case_90

Install Handle

Get the handle ready to install into the two halves of the case.

Orient the handle with the case halves.

Insert the handle into handle notches on the back half of the case.

handle_91

handle_92

handle_93

Close Case

Carefully join the two halves of the case together, making sure all of the wires are neatly tucked inside.

close_94

close_95

close_96

Secure Case Halves

Use the following screws to secure the case halves together.

  • 4x M3 x 4mm long machine screws

  • 1x M3 x 20mm long machine screw

Insert the four short screws to the counter-sunk holes on the back of the case.

Insert the longer screw to the top corner hole, near the wheel holder assembly.

halves_97

halves_98

halves_99

Assembled Case

Check the two halves of the case are secured together.

assembled_100

Install Key Switch Cover

Orient the two prongs from the key switch button cover with the choc key switch. Press down to fit the cover onto the key switch.

key_101

key_102

Install Slider Knob

Orient the slide knob with the nub on the slide potentiometer. Press the knob into the nub.

knob_103

knob_104

knob_105

Usage

Power Device

Use the slide switch to power the device on or off.

3d_printing_power

Start Game

Push the button key switch to start a new game.

3d_printing_start

Gameplay

Push, flip, slide, spin or shake whenever the LED display and audio prompts you before the timer runs out.

If you make the correct move, the NeoPixels turn green and the LED display shows your score.

If the timer runs out before you make a move or if you make the wrong move, the NeoPixels turn red and the game is over.

At the end of each game, the LED display shows your total score. You can start a new game after the score has been displayed.

3d_printing_gameplay

Mfr Part # 5768
ADAFRUIT RP2040 PROP-MAKER FEATH
Adafruit Industries LLC
Mfr Part # 2158
ADDRESS LED 14 SEGMENT I2C YLW
Adafruit Industries LLC
Mfr Part # 1426
ADDRESS LED MODULE SERIAL RGB
Adafruit Industries LLC
Mfr Part # 5114
KAILH CHOC LOW PROF WHITE 1=10pc
Adafruit Industries LLC
Mfr Part # 3221
MINI PANEL MOUNT SPDT TOGGLE SWI
Adafruit Industries LLC
Mfr Part # 1178
RADIAL BALL BEARING 608ZZ 4PK
Adafruit Industries LLC
Mfr Part # 1781
BATTERY LITH-ION 3.7V 2.2AH 1865
Adafruit Industries LLC
Mfr Part # 3923
SPEAKER 8OHM 1W TOP PORT
Adafruit Industries LLC
Mfr Part # 805
BREADBOARD-FRIENDLY SPDT SLIDE S
Adafruit Industries LLC
Mfr Part # 4720
1.25MM PITCH 2-PIN CABLE MATCHIN
Adafruit Industries LLC
Mfr Part # 4210
JST SH 4-PIN CABLE - QWIIC COMPA
Adafruit Industries LLC
Mfr Part # 2001
HOOK-UP 30AWG 600V RED 6.56'
Adafruit Industries LLC
Mfr Part # 291
MAGNIFIER STAND 2.5" 4X
Adafruit Industries LLC
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.