Built-in project · Raspberry Pi Pico

Stop the light game on a Raspberry Pi Pico

Stop the light game on a Raspberry Pi Pico: press the button as a red light runs onto the green LED of a 16-LED NeoPixel ring, faster every level. The whole build, a Raspberry Pi Pico and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Beginner Runs in your browser. Free, and no account needed.

The parts retail for about $8.90. On Mokxi they're free.

Stop the light game · Raspberry Pi Picolive0.000 s 0.00x
Press Run, then press the button to start
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Raspberry Pi Pico
// Stop the light: an arcade timing game on a ring of 16 NeoPixels and a
// Raspberry Pi Pico. A red light runs round the ring. Press the button just
// as it lands on the green LED.
//
//   LED ring   DIN -> GP22, 5V -> VBUS, GND -> GND (16 WS2812B LEDs)
//   button     GP14 and GND (INPUT_PULLUP: a press reads LOW)
//   piezo      GP7 and GND
//
// Catch it and you go up a level: the light runs faster. Miss and the game is
// over; the ring shows where the light stopped and how far off you were. The
// best level is shown on the Serial Monitor. While no game is running the
// ring spins a rainbow; press the button to start.
//
// # Where is the light?
//
// The light's position is worked out from the time, not counted step by
// step: (millis since the level started) / (ms per LED), round the ring.
// That keeps the speed exact however long the rest of loop() takes, and the
// moment the button goes down the sketch knows exactly which LED was lit.

#include <Adafruit_NeoPixel.h>

const uint8_t RING_PIN = 22;
const uint8_t BUTTON = 14;
const uint8_t PIEZO = 7;
const uint8_t LEDS = 16;
const uint8_t TARGET = 0;  // the green LED
const uint8_t START = 8;   // the light starts opposite it

Adafruit_NeoPixel ring(LEDS, RING_PIN, NEO_GRB + NEO_KHZ800);

uint8_t level = 1;
uint8_t best = 0;
bool playing = false;
unsigned long levelStarted = 0;
bool lastButton = HIGH;
unsigned long buttonAt = 0;

// Each level the light is 15% faster, never quicker than 25 ms an LED.
unsigned long stepMs() {
  unsigned long ms = 150;
  for (uint8_t i = 1; i < level; i++) ms = ms * 85 / 100;
  return ms < 25 ? 25 : ms;
}

uint8_t lightAt(unsigned long now) {
  return (START + (now - levelStarted) / stepMs()) % LEDS;
}

// True once for each new press, ignoring 30 ms of contact bounce.
bool pressed() {
  bool now = digitalRead(BUTTON);
  bool edge = now == LOW && lastButton == HIGH && millis() - buttonAt > 30;
  if (now != lastButton) buttonAt = millis();
  lastButton = now;
  return edge;
}

void drawGame(uint8_t light) {
  ring.clear();
  ring.setPixelColor(TARGET, ring.Color(0, 150, 0));
  ring.setPixelColor(light, ring.Color(255, 0, 0));
  ring.show();
}

void startLevel() {
  Serial.print("level ");
  Serial.print(level);
  Serial.print(": ");
  Serial.print(stepMs());
  Serial.println(" ms an LED");
  // The button was let go while the ring flashed; take its state as it is now,
  // so the next press counts at once.
  lastButton = digitalRead(BUTTON);
  levelStarted = millis();
}

void caught() {
  Serial.print("caught it on level ");
  Serial.println(level);
  // the whole ring flashes green, with a rising chirp
  for (uint8_t i = 0; i < 3; i++) {
    ring.fill(ring.Color(0, 200, 0));
    ring.show();
    tone(PIEZO, 1000 + i * 400, 80);
    delay(120);
    ring.clear();
    ring.show();
    delay(60);
  }
  level++;
  startLevel();
}

void missed(uint8_t light) {
  // how many LEDs off, the short way round
  uint8_t off = light > LEDS / 2 ? LEDS - light : light;
  Serial.print("stopped on LED ");
  Serial.print(light);
  Serial.print(", missed by ");
  Serial.print(off);
  Serial.print(off == 1 ? " LED" : " LEDs");
  Serial.print(": game over on level ");
  Serial.println(level);
  if (level > best) best = level;
  Serial.print("best level: ");
  Serial.println(best);
  tone(PIEZO, 140, 600);
  // show where it stopped, blinking
  for (uint8_t i = 0; i < 4; i++) {
    drawGame(light);
    delay(250);
    ring.setPixelColor(light, 0);
    ring.show();
    delay(250);
  }
  playing = false;
  Serial.println("press the button to start");
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON, INPUT_PULLUP);
  pinMode(PIEZO, OUTPUT);
  ring.begin();
  ring.setBrightness(60);
  Serial.println("Stop the light: press the button to start");
}

void loop() {
  unsigned long now = millis();
  if (!playing) {
    // a slow rainbow while it waits
    for (uint8_t i = 0; i < LEDS; i++) {
      ring.setPixelColor(i, ring.ColorHSV((uint16_t)(i * 65536L / LEDS + now * 20)));
    }
    ring.show();
    if (pressed()) {
      playing = true;
      level = 1;
      startLevel();
    }
    return;
  }
  uint8_t light = lightAt(now);
  drawGame(light);
  if (pressed()) {
    if (light == TARGET) {
      caught();
    } else {
      missed(light);
    }
  }
}

Parts list

5 parts, plus the jumper wires. They retail for about $8.90; on Mokxi they're free.

How it is wired

7 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Ground: Raspberry Pi Pico pin GND; Piezo speaker pin 2
  • Raspberry Pi Pico pin GP7; Piezo speaker pin 1
  • Raspberry Pi Pico pin GP14; Arcade button pin 2
  • Raspberry Pi Pico pin GP22; Addressable RGB LEDs pin DIN
  • 5 V: Addressable RGB LEDs pin VDD
  • Ground: Addressable RGB LEDs pin VSS
  • Ground: Arcade button pin 1

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.