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()

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

This page (Pixel Chase Game CircuitPython Code Walkthrough) was last updated on Jul 27, 2020.

Text editor powered by tinymce.