Prop It Game
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.
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.
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
Quad Alphanumeric Display - Yellow 0.54" Digits w/ I2C Backpack
1 x Hall effect sensor
1 x Slide Switch
1 x STEMMA QT Cable
4 x Neodymium Magnet
1 x 10K Resistor
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.
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.
Build Volume
The parts require a 3D printer with a minimum build volume of 100 (X) x 135 (Y) x 31mm (Z).
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.
3D Models of Adafruit Parts
Electronic components like Adafruit's boards and more can be downloaded from the Adafruit CAD parts GitHub Repo.
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.
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.
The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.
That's it, you're done! :)
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.
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.
# 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()
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:
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.
# 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.
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.
# 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.
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_xA few states and variables are defined before the loop.
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.
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 gameGameplay
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.
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.
# 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:
breakWinning 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.
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.
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.
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.
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.
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.
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.
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)
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 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.
Mount Key Switch
Orient the choc key switch and press fit it into the square cutout on the front half of the enclosure.
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.
Input Control Signal Wires
Prepare three multi-color wires (white, green, yellow and red) about 4.5 inches (11.5cm) for the input controls.
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.
Solder Wire to Toggle
Solder the white wire to the either far left or right pin on the toggle switch.
Solder Wire to Key Switch
Solder the green wire to the remaining pin on the choc key switch.
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.
Feather Pre-solder
Get the Feather ready to solder the signal wires.
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.
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.
NeoPixel to Feather
Get the NeoPixel stick ready to connect to the Feather.
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.
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.
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.
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.
Check Wired Connections
Take a moment to inspect your work, making sure all of the wires have been properly soldered and secured.
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.
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.
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.
Secure Slide Switch
Insert the slide switch to the holder built into the back half of the enclosure.
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.
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.
Connect STEMMA from LED Backpack
Connect the STEMMA QT cable from the LED display to the STEMMA QT port on the Feather.
Secure Speaker to Case
Place the speaker over the speaker holder on the back half of the case with it facing up.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
Case Check
Take a moment to spect all of the parts are fitting together correctly.
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.
Close Case
Carefully join the two halves of the case together, making sure all of the wires are neatly tucked inside.
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.
Assembled Case
Check the two halves of the case are secured together.
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.
Install Slider Knob
Orient the slide knob with the nub on the slide potentiometer. Press the knob into the nub.
Usage
Power Device
Use the slide switch to power the device on or off.
Start Game
Push the button key switch to start a new game.
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.

