Arcade Inspired Game

Building an arcade game with NeoPixels and CircuitPython! This project is inspired by the “cyclone” LED chase games often found in arcades. The enclosure is 3D printed and snap fits together. Inside is an Adafruit Feather, an arcade button, a rechargeable battery and a slide switch.

Game Goals & Rules

The goal is to press the button when the LED lands on the target pixels. The game advances and speeds up as you score and hit the targets. The running pixel changes color as you level up and goes in the order of ROYGBIV.

The target pixels are placed randomly so it’s different for each level. If you miss a target, the LEDs flash red and the game starts over with the slow speed.

Parts

Angled shot of a Adafruit Feather M4 Express.
It's what you've been waiting for, the Feather M4 Express featuring ATSAMD51. This Feather is fast like a swift, smart like an owl, strong like a ox-bird (it's half ox,...
Out of Stock
Adafruit NeoPixel Digital RGB LED Strip with all the LEDs in a rainbow
So thin. So mini. So teeeeeeny-tiny. It's the 'skinny' version of our classic NeoPixel strips!These NeoPixel strips have 144 digitally-addressable pixel Mini LEDs...
$59.95
In Stock
Video of 24mm mini translucent green LED arcade button flashing on and off.
A button is a button, and a switch is a switch, but these translucent arcade buttons are in a class of their own. Particularly because they have LEDs built right in!...
$2.50
In Stock
Lithium Ion Polymer Battery 3.7v 420mAh with JST 2-PH connector and short cable
Lithium-ion polymer (also known as 'lipo' or 'lipoly') batteries are thin, light, and powerful. The output ranges from 4.2V when completely charged to 3.7V. This...
Out of Stock
1 x 10-wire ribbon cable
silicone cover stranded-core
1 x slide switch
breadboard friendly
1 x JST Extension cable
2-pin JST cable
1 x Quick-Connects
Arcade Button Quick-Connect Wire Pairs - 0.11" (10 pack)
1 x USB cable
USB cable - USB A to Micro-B

The diagram below provides a visual reference for wiring of the components. This diagram was created using the software package Fritzing.

Adafruit Library for Fritzing

Use Adafruit's Fritzing parts library to create circuit diagrams for your projects. Download the library or just grab individual parts. Get the library and parts from GitHub - Adafruit Fritzing Parts.

Wired Connections

  • 5V from LED Strip to 3V on Feather
  • GND from LED Strip to GND on Feather
  • DIN from LED Strip to Pin #6 on Feather
  • Button to Pin #5 on Feather
  • Button to GND on Feather
  • Switch to GND on Feather
  • Switch to EN on Feather

Powering

The Adafruit board can be powered via USB or JST using a 3.7v lipo battery. In this project, a 420mAh lipo battery is used. The lipo battery is rechargeable via the USB port on the board. The switch is wired to the enable and ground pins on the board.

Parts List

STL files for 3D printing are oriented to print "as-is" on FDM style machines. Parts are designed to 3D print without any support material. Original design source may be downloaded using the links below.

  • shell-led-chase.stl
  • lid-led-chase.stl
  • diffuser-led-chase.stl
  • button-sleve.stl

Slicing Parts

No supports are required. Slice with settings for PLA material.

The parts were sliced using CURA using the slice settings below.

  • PLA filament 220c extruder
  • 0.2 layer height
  • 10% gyroid infill
  • 90mm/s print speed
  • 60c heated bed

Design Source Files

The project assembly was designed in Fusion 360. This can be downloaded in different formats like STEP, STL and more. Electronic components like Adafruit's board, displays, connectors and more can be downloaded from the Adafruit CAD parts GitHub Repo.

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.

The following instructions will show you how to install CircuitPython. If you've already installed CircuitPython but are looking to update it or reinstall it, the same steps work for that as well!

Set up CircuitPython Quick Start!

Follow this quick step-by-step for super-fast Python power :)

Click the link above and download the latest UF2 file.

Download and save it to your desktop (or wherever is handy).

Plug your Feather M4 into your computer using a known-good USB cable.

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

Double-click the Reset button next to the USB connector on your board, and you will see the NeoPixel RGB LED turn green. If it turns red, check the USB cable, try another USB port, etc. Note: The little red LED next to the USB connector will pulse red. That's ok!

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

You will see a new disk drive appear called FEATHERBOOT.

 

 

 

Drag the adafruit_circuitpython_etc.uf2 file to FEATHERBOOT.

The LED will flash. Then, the FEATHERBOOT drive will disappear and a new disk drive called CIRCUITPY will appear.

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

Further Information

For more detailed info on installing CircuitPython, check out Installing CircuitPython.

Installing Project Code

To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.

Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory Pixel_Chase_Game/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.

Your CIRCUITPY drive should now look similar to the following image:

CIRCUITPY
# SPDX-FileCopyrightText: 2020 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
import random
import board
from rainbowio import colorwheel
import neopixel
import digitalio
import adafruit_led_animation.color as color

#  button pin setup
button = digitalio.DigitalInOut(board.D5)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP

#  neopixel setup
pixel_pin = board.D6
num_pixels = 61

pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.2, auto_write=False)

def rainbow_cycle(wait):
    for j in range(255):
        for i in range(num_pixels):
            rc_index = (i * 256 // 10) + j
            pixels[i] = colorwheel(rc_index & 255)
        pixels.show()
        time.sleep(wait)

#  color_chase setup
def color_chase(c, wait):
    for i in range(num_pixels):
        pixels[i] = c
        time.sleep(wait)
        pixels.show()
    time.sleep(0.5)

#  function to blink the neopixels when you lose
def game_over():
    color_chase(color.BLACK, 0.05)
    pixels.fill(color.RED)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.BLACK)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.RED)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.BLACK)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.RED)
    pixels.show()
    time.sleep(1)

#  variables and states
pixel = 0
num = 0
last_num = 0
now_color = 0
next_color = 1
speed = 0.1
level = 0.005
final_level = 0.001
new_target = True
button_state = False

#  neopixel colors
colors = [color.RED, color.ORANGE, color.YELLOW, color.GREEN, color.TEAL, color.CYAN,
          color.BLUE, color.PURPLE, color.MAGENTA, color.GOLD, color.AQUA, color.PINK]

while True:

    #  button debouncing
    if not button.value and not button_state:
        button_state = True

    #  if new level starting..
    if new_target:
        #  randomize target location
        y = int(random.randint(5, 55))
        x = int(y - 1)
        z = int(y + 1)
        new_target = False
        print(x, y, z)
    pixels[x] = color.WHITE
    pixels[y] = colors[next_color]
    pixels[z] = color.WHITE
    #  delay without time.sleep()
    if (pixel + speed) < time.monotonic():
        #  turn off pixel behind chaser
        if num > 0:
            last_num = num - 1
            pixels[last_num] = color.BLACK
            pixels.show()
        #  keep target pixels their colors when the chaser passes
        if last_num in (x, y, z):
            pixels[x] = color.WHITE
            pixels[y] = colors[next_color]
            pixels[z] = color.WHITE
        #  move chaser pixel by one
        if num < num_pixels:
            pixels[num] = colors[now_color]
            pixels.show()
            #print(num)
            #print("target is", y)
            num += 1
        #  send chaser back to the beginning of the circle
        if num == num_pixels:
            last_num = num - 1
            pixels[last_num] = color.BLACK
            pixels.show()
            num = 0
        #  if the chaser hits the target...
        if last_num in [x, y, z] and not button.value:
            button_state = False
            #  fills with the next color
            pixels.fill(colors[next_color])
            pixels.show()
            print(num)
            print(x, y, z)
            #  chaser resets
            num = 0
            time.sleep(0.5)
            pixels.fill(color.BLACK)
            pixels.show()
            #  speed increases for next level
            speed = speed - level
            #  color updates
            next_color = next_color + 1
            if next_color > 11:
                next_color = 0
            now_color = now_color + 1
            if now_color > 11:
                now_color = 0
            #  setup for new target
            new_target = True
            print("speed is", speed)
            print("button is", button.value)
        #  if the chaser misses the target...
        if last_num not in [x, y, z] and not button.value:
            button_state = False
            print(num)
            print(x, y, z)
            #  fills with current chaser color
            pixels.fill(colors[now_color])
            pixels.show()
            #  function to flash all pixels red
            game_over()
            #  chaser is reset
            num = 0
            pixels.fill(color.BLACK)
            pixels.show()
            #  speed is reset to default
            speed = 0.1
            #  colors are reset
            next_color = 1
            now_color = 0
            #  setup for new target
            new_target = True
            print("speed is", speed)
            print("button is", button.value)
        #  when you have beaten all the levels...
        if speed < final_level:
            #  rainbows!
            rainbow_cycle(0.01)
            time.sleep(1)
            #  chaser is reset
            num = 0
            pixels.fill(color.BLACK)
            pixels.show()
            #  speed is reset to default
            speed = 0.1
            #  colors are reset
            next_color = 1
            now_color = 0
            #  setup for new target
            new_target = True
        #  time.monotonic() is reset for the delay
        pixel = time.monotonic()

Import the Libraries

First, the CircuitPython libraries are imported. The adafruit_led_animation library is being used as a way to easily access different colors for the NeoPixels.

import time
import random
import board
import neopixel
import digitalio
import adafruit_led_animation.color as color

Setup the Button and NeoPixels

Next, the button's pin is setup.

#  button pin setup
button = digitalio.DigitalInOut(board.D5)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP

Followed by the NeoPixel setup.

#  neopixel setup
pixel_pin = board.D6
num_pixels = 61

pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.2, auto_write=False)

NeoPixel Animations

Three functions are brought in to continue the NeoPixel setup. All three of them are classic NeoPixel animations: rainbow_cycle and color_chase.

#  wheel and rainbow_cycle setup
def wheel(pos):
    if pos < 0 or pos > 255:
        return (0, 0, 0)
    if pos < 85:
        return (255 - pos * 3, pos * 3, 0)
    if pos < 170:
        pos -= 85
        return (0, 255 - pos * 3, pos * 3)
    pos -= 170
    return (pos * 3, 0, 255 - pos * 3)

def rainbow_cycle(wait):
    for j in range(255):
        for i in range(num_pixels):
            rc_index = (i * 256 // 10) + j
            pixels[i] = wheel(rc_index & 255)
        pixels.show()
        time.sleep(wait)

#  color_chase setup
def color_chase(c, wait):
    for i in range(num_pixels):
        pixels[i] = c
        time.sleep(wait)
        pixels.show()
    time.sleep(0.5)

These animation functions are followed by the game_over() function. This function allows for the NeoPixel strip to use color_chase to turn off the NeoPixels and then blink the strip red when you lose a level in the game. The reason for setting it up as a function rather than in the loop is to keep the loop easier to read since there will be other things going on.

#  function to blink the neopixels when you lose
def game_over():
    color_chase(color.BLACK, 0.05)
    pixels.fill(color.RED)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.BLACK)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.RED)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.BLACK)
    pixels.show()
    time.sleep(0.5)
    pixels.fill(color.RED)
    pixels.show()
    time.sleep(1)

Variables and States

Next are the variables and state machines that will be used in the loop. Their functions are commented next to them.

#  variables and states
pixel = 0 #  time.monotonic() holder
num = 0 #  chaser NeoPixel position
last_num = 0 #  previous chaser position
now_color = 0 #  chaser NeoPixel color
next_color = 1 #  target NeoPixel color
speed = 0.1 #  default speed for chaser
level = 0.005 #  speed increase increment
final_level = 0.001 #  final level speed
new_target = True #  state to denote a new level
button_state = False #  button debouncing state

Colors

The last piece of setup before the loop is the NeoPixel colors. Using the adafruit_led_animation library, you can insert colors easily to assign to the NeoPixels without having to determine RGB values. 

This array of colors will be used to cycle through colors as you advance through the game, using now_color and next_color to index your position in the array. now_color will be the color of the chaser pixel and next_color will be the color of the target pixel. 

#  neopixel colors
colors = [color.RED, color.ORANGE, color.YELLOW, color.GREEN, color.TEAL, color.CYAN,
          color.BLUE, color.PURPLE, color.MAGENTA, color.GOLD, color.AQUA, color.PINK]

The Loop

The loop begins with an if statement for button debouncing.

#  button debouncing
    if not button.value and not button_state:
        button_state = True

Randomized Target

x, y and z hold the target pixel positions. y is setup to hold a random integer within the range of 5 and 55. This position is reset every time you advance a level. x and z are setup to be on either side of y.

x and z are setup to be white to highlight the target pixel, which is setup to be the next_color index in the colors array.

#  if new level starting..
    if new_target:
        #  randomize target location
        y = int(random.randint(5, 55))
        x = int(y - 1)
        z = int(y + 1)
        new_target = False
        print(x, y, z)
    pixels[x] = color.WHITE
    pixels[y] = colors[next_color]
    pixels[z] = color.WHITE

Playing the Game

Game play begins using time.monotonic() instead of time.sleep() to delay the loop. time.sleep() delays the entire loop, where as when you use time.monotonic(), the timing is tracked without stopping the entire loop.

#  delay without time.sleep()
if (pixel + speed) < time.monotonic():

Chaser Pixel Animation

Before getting into hitting pressing the button to hit the target NeoPixel, there are a few things that need to happen in the code in order for the chaser pixel to move.

First, when the chaser pixel (tracked with num) moves forward, the previous pixel is turned off. This previous pixel is tracked with last_num.

#  turn off pixel behind chaser
if num > 0:
    last_num = num - 1
    pixels[last_num] = color.BLACK
    pixels.show()

You also want to keep the target pixels their preset colors, even as the chaser pixel passes them.

#  keep target pixels their colors when the chaser passes
if last_num in (x, y, z):
    pixels[x] = color.WHITE
    pixels[y] = colors[next_color]
    pixels[z] = color.WHITE

How does the chaser pixel move though? While the position of the chaser pixel is less than the total number of NeoPixels, it advances by one pixel position.

#  move chaser pixel by one
if num < num_pixels:
    pixels[num] = colors[now_color]
    pixels.show()
    #print(num)
    #print("target is", y)
    num += 1

When the chaser reaches the end of the NeoPixel strip, num is reset to 0 to send the chaser back to the beginning of the strip.

#  send chaser back to the beginning of the circle
if num == num_pixels:
    last_num = num - 1
    pixels[last_num] = color.BLACK
    pixels.show()
    num = 0

Level-Up

Using the button, you'll try and line-up the chaser pixel with the target pixel to score. When you score, all of the NeoPixels will light-up with the next_color, which the target pixel had been showing.

The position of the chaser pixel is reset to 0 and the speed is increased by the increment stored in level. The now_color and next_color indexes are also increased by 1. Finally, new_target is set to True in order to setup a new target pixel.

#  if the chaser hits the target...
        if last_num in [x, y, z] and not button.value:
            button_state = False
            #  fills with the next color
            pixels.fill(colors[next_color])
            pixels.show()
            print(num)
            print(x, y, z)
            #  chaser resets
            num = 0
            time.sleep(0.5)
            pixels.fill(color.BLACK)
            pixels.show()
            #  speed increases for next level
            speed = speed - level
            #  color updates
            next_color = next_color + 1
            if next_color > 11:
                next_color = 0
            now_color = now_color + 1
            if now_color > 11:
                now_color = 0
            #  setup for new target
            new_target = True
            print("speed is", speed)
            print("button is", button.value)

Missing the Target

If you miss the target pixel with your button press, all of the NeoPixels will light-up with the now_color. This is the same color that the chaser pixel had been.

Then, the game_over() function animates the NeoPixels by using color_chase to turn all of the pixels off and then flash them all red.

To set things up for a new game, the position of the chaser pixel is reset to 0 and the speed is reset to its default value. The now_color and next_color indexes are also reset. Finally, new_target is set to True in order to setup a new target pixel.

#  if the chaser misses the target...
        if last_num not in [x, y, z] and not button.value:
            button_state = False
            print(num)
            print(x, y, z)
            #  fills with current chaser color
            pixels.fill(colors[now_color])
            pixels.show()
            #  function to flash all pixels red
            game_over()
            #  chaser is reset
            num = 0
            pixels.fill(color.BLACK)
            pixels.show()
            #  speed is reset to default
            speed = 0.1
            #  colors are reset
            next_color = 1
            now_color = 0
            #  setup for new target
            new_target = True
            print("speed is", speed)
            print("button is", button.value)

You Win!

If you happen to be an expert at the NeoPixel Run Game, you'll defeat all of the targets with your chaser pixel and eventually run out of levels. You'll know you've won when all of the NeoPixels animate using the classic rainbow_cycle() animation.

After some fun rainbows, all of the game parameters are reset to their defaults so that you can begin playing again.

#  when you have beaten all the levels...
        if speed < final_level:
            #  rainbows!
            rainbow_cycle(0.01)
            time.sleep(1)
            #  chaser is reset
            num = 0
            pixels.fill(color.BLACK)
            pixels.show()
            #  speed is reset to default
            speed = 0.1
            #  colors are reset
            next_color = 1
            now_color = 0
            #  setup for new target
            new_target = True

Time Tracking

The last line of the loop updates pixel to grab the current time.monotonic() time.

#  time.monotonic() is reset for the delay
pixel = time.monotonic()

Solder LED Strip

Cut a high density Neopixel strip to 61 pixels and use silicone ribbon cable to help keep the wires bundled.

Tin the pads on the NeoPixel strip and solder wires to the data and power and ground pads.

Slide switch 

Tin two pins on the slide switch. Add heat shrink to each wire and then solder to each pin on the slide switch. 

Use the side of the solder iron to heat and shrink the connections.

Solder quick connects

Use one of the ground connections close to the prototyping area to solder the quick connect wires for the button. The other wire connects to pin 5.

Test Circuit

Connecting the button and battery to the Feather to quickly test the connections. 

 

Mount Feather

Orient the Feather board so the USB connection is facing the cut out on the case.

Use four M2.5 x 5mm long screws to secure the Feather board to the standoffs on the case.

Mount LED strip

Pass the LED strip wires through the cutoff on the inside of the case. Wrap the LED strip along the inner wall of the case.

Use kapton tape to cover the pads on the end of the NeoPixel strip so the pads doesn't short the circuit. 

Mount slide switch

Place the switch to the middle position and then angle the whole slide switch so it can press fit between the three walls on the case.

 

Mount diffuser

Arrange the printed diffuser with the bigger lip face up. Gently bend the diffuser so it can press fit over the LED strip. 

Plug in Battery

Plug in the JST extension cable to the Feather board and then attach the battery. Use foam tape to secure the battery to the case. 

Attach Shelf

Use pliers to slightly bend the connections to fit inside the case.

Mount Button

Attach the printed button shelf to the button.

Align the button with the crown icon as shown. Pass the button at an angle and then twist clockwise to mount the button. 

Connect button

Attach the quick connect wires to the button. The wires attach to the connections close the points on the crown icon.

Align Lid

Match the cut out on the lid to the slide switch. Use slight force to press fit each nub around the lid on to the case.

This guide was first published on Jul 28, 2020. It was last updated on Jul 20, 2024.