← home portfolio
HI, I'M UNDER CONSTRUCTION, PLEASE EXCUSE THE ROUGH EXTERIOR—I PROMISE MY CONTENT IS (MOSTLY) GOOD <3

ITP Fall '21 — PCOMP — Project 1

Project FNO.

FNO, like oh eff no how's my memory this bad!

A memory game similar to Simon Says where a pattern is played in its entirety, and the player has to input the exact same pattern back. They have three chances to do so, and each incorrect input causes them to lose a life.

Team members

Ideation

When we first got together, we had many different ideas: Vesper imagined a light-up maze with many buttons to navigate with, Jingjing wanted to create a device that could let her family know when she got out of bed in the morning, and I wanted to create a PCOMP version of my boba fund (when I make boba/milk tea at home instead of going out, I pay myself $5).

Some initial ideas (left), and my very questionable drawing when I pitched the final idea to Vesper and Jingjing (right).

We doodled a few ideas, including a narrative that we could "scroll through" with a potentiometer, and the corresponding panel in the story would light up (left).

Ultimately we went with an idea that would be interesting from a PCOMP perspective: a memory game with three LEDs and three corresponding buttons, as well as another three LEDs indicating remaining lives, and a reset button.

Wiring

We started off with the pseudo-code to figure out exactly what components we needed to include and how to wire them:

// a game with 3 lights corresponding to 3 buttons
// and another 3 lights representing one life each, and a button to reset the game
// finally, a toggle switch for turning game on/off (no need to program it)
// it will light up a pattern by blinking 6 times in different colors
// then player tries to repeat the pattern with the buttons
// if they get the pattern wrong, they lose a "life" until they have no life left
// when they get the full pattern right, the lights will dance!

// declare arrays to store state for
// 1. the pattern (length 6 of integers)
// 2. the player's input pattern so far (max length 6 of integers)
// 3. an integer starting at 2 of how many lives left
// 4. previous input button states (length 3 of booleans, defaulting to false at start)

void setup() {
// begin Neopixel strip
}

void loop() {
// check if reset button is pressed
// if it is, call resetGame(), return

// if pattern is empty, setup pattern
setupPattern();

// loop through all 3 input buttons and see if any were pressed
// that weren't already pressed.
// store new button states in button state array
// light up corresponding light for each pressed button

// if more than one button is pressed at a time,
// treat that as incorrect and they lose a life

// if none are pressed, return and do nothing

// if one is pressed, add button number to input pattern array
// and check whether the patterns match
boolean matches = checkPlayerPattern();

// if pattern does not match, check number of lives left
// if more than 1 life, decrease number of lives
// if only 1 life left, then play the death pattern
// reset game

// if pattern does match, check if pattern is complete (input pattern = length 6)
// if yes, play celebration pattern, reset game
// if no, continue

}

// function to set up a pattern of random integers of 1, 2, 3
void setupPattern() {
// loop through pattern array, randomly set number
}

// function to check if player input matches pattern
boolean checkPlayerPattern() {
// loop through input pattern array, check if they match pattern array
}

// function to reset entire game
void resetGame() {
// set pattern and input array to empty
// set number of lives back to 2
// set button states all back to false
}

Based on the pseudo-code, we'd need:

I had found a Neopixel strip on the junk shelf the first week (fully soldered, yay!) that worked well for the LEDs, and sourced the pushbuttons and toggle switch from the yellow bins in the shop. Additionally, Vesper had found a battery on the junk, so we figured we were good to go.

We started by laying out the circuit in Fritzing (we couldn't find the Neopixel in Fritzing so substituted it with a servo motor which seemed to have a similar set of pins: power, ground, and data in):

Our original Fritzing sketch and wiring.

When we went to actually wire, we thought (incorrectly—more on this in the batteries section) that we needed to bring the 9V down to 5V to not overwhelm our Arduino Uno and the rest of our components. So we spent a (painful) while trying to figure out how to place a voltage regulator between our battery and our on/off toggle switch, that would then power the rest of the board:

The battery → voltage regulator → toggle switch wiring that finally worked (left), and verifying with a multimeter that the board was getting power (middle) and that the pushbuttons were also working as expected (right).

We finally figured out the wiring: both wires from the battery going into the voltage regulator, then the ground from the regulator going to the board's ground and the regulated voltage going to one of the button/switch's legs. Then the button's other pin connects to the board's power.

I also incorrectly thought that every pushbutton/toggle switch would need a pull-down resistor to prevent a short circuit. It turns out that not only did we not need it (my best guess is that when the button isn't on, it's preventing power from flowing into the circuit and thus wouldn't cause a short-circuit anyways), it also cut down the voltage being supplied to the board from 5V down to ~2.5V.

Code

We decided to temporarily stop worrying about the voltage being supplied by the battery, and switch instead to coding—where our power is supplied to the breadboard by our laptop via the Arduino.

We started by making sure that the Neopixel strip we salvaged from the junk shelf was still working by running this code from Adafruit:

TODO: add in Neopixel code

The code lights four "pixels" at a time, moving down the whole strip first in red, then green, then blue.

To our delight, it works, so we move onto coding a random pattern and having it play on our 3 lights. Here, we ran into two problems:

  1. Arduino's random() isn't very random. It seems to also follow the same pattern every single time the Arduino is restarted.
  2. Even after we generated our pattern we couldn't get our corresponding Neopixels to light up. After much banging our head, we realized we forgot to call strip.show() after setting pixel colors. Always call strip.show() or Neopixel won't do anything!
Tom has suggested (joked?) we'd get a better approximation of random by just leaving a pin open and it'll have more noise.

After we're able to figure those two problems out, we:

  1. Read the button inputs and store their state so we only register a button press and store it in the input pattern on state change (and not for every moment the button is pressed down)
  2. Hook up the button presses to turn on the corresponding Neopixel
  3. Loop through both the pattern and inputPattern arrays and see if player has input the correct pattern
  4. Code all Neopixels to blink red if the inputted pattern is incorrect, and a celebratory purple if correct
  5. Hook up lights for the three lives, and reset behavior when the reset button is pressed.
Pressing a pushbutton lights up the corresponding Neopixel (left), and the wrong pattern triggers blinking red lights (right).

Here's the final code:

// a game with 3 lights corresponding to 3 buttons
// and another 3 lights representing one life each, and a button to reset the game
// finally, a toggle switch for turning game on/off (no need to program it)
// it will light up a pattern by blinking 6 times in different colors
// then player tries to repeat the pattern with the buttons
// if they get the pattern wrong, they lose a "life" until they have no life left
// when they get the full pattern right, the lights will dance!

// include neopixel library
#include <Adafruit_NeoPixel.h>

#define PIN 3
#define N_LEDS 7
#define patternLength 6
#define buttonLength 3
#define totalLives 3
#define resetPin 8

Adafruit_NeoPixel strip = Adafruit_NeoPixel(N_LEDS, PIN, NEO_GRB + NEO_KHZ800);

// set LED colors to red, green, yellow
const uint32_t ledColors[3] = {strip.Color(255, 0, 0), strip.Color(0, 255, 0), strip.Color(255, 128, 0)};
// array of input button pins
const int buttonPins[buttonLength] = {13, 12, 11};

// declare arrays to store state for
// # the pattern (length 6 of integers)
int pattern[patternLength];
// # the player's input pattern so far (max length 6 of integers)
int inputPattern[patternLength];
// # input pattern position
int patternPosition = 0;
// # an integer starting at 2 of how many lives left
int numLives = 3;
// # previous input button states (length 3 of booleans, defaulting to false at start)
// BECAUSE WE'RE USING PULLUP, THE READINGS ARE REVERSED, unpressed is HIGH
int prevButtonState[buttonLength] = {HIGH, HIGH, HIGH};
// previous reset button state
int prevResetState = HIGH;

void setup() {
// begin Neopixel strip
strip.begin();
// set button pin modes
for (int i = 0; i < buttonLength; i += 1) {
pinMode(buttonPins[i], INPUT_PULLUP);
}
pinMode(resetPin, INPUT_PULLUP);

Serial.begin(9600);
}

void loop() {
// check if reset button is pressed
// if it is, call resetGame(), return
int resetReading = digitalRead(resetPin);
if (resetReading != prevResetState) {
if (resetReading == HIGH) {
// if reset button was pressed (LOW) and released (HIGH)
resetGame();
Serial.println("reset");
}
}
prevResetState = resetReading;

// if pattern is empty, setup pattern
if (checkArrayEmpty(pattern)) {
clearNeopixel();
lightupLives();
setupPattern();
}

// loop through all 3 input buttons and see if any were pressed
// that weren't already pressed.
// store new button states in button state array
// light up corresponding light for each pressed button
int buttonPressed = 0;
for (int i = 0; i < buttonLength; i += 1) {
int buttonReading = digitalRead(buttonPins[i]);

if (buttonReading != prevButtonState[i]) {
// if button state has changed, light up LED accordingly
if (buttonReading == LOW) {
// if button is pressed, we want to turn on pixel
strip.setPixelColor(i + 1, ledColors[i]);
// and remember it was pressed
buttonPressed = i + 1;
} else if (buttonReading == HIGH) {
// if button is released, turn off pixel
strip.setPixelColor(i + 1, 0);
}
strip.show();

Serial.print(buttonPins[i]);
Serial.print(" is ");
Serial.println(buttonReading);
}

prevButtonState[i] = buttonReading;
}


// if more than one button is pressed at a time,
// treat that as incorrect and they lose a life

// if none are pressed, return and do nothing
if (buttonPressed) {
// if one is pressed, add button number to input pattern array
// and check whether the patterns match
inputPattern[patternPosition] = buttonPressed - 1;
patternPosition += 1;
boolean matches = checkPlayerPattern();

if (!matches) {
// if pattern is wrong, blink red
for (int i = 0; i < 1; i += 1) {
blinkRed();
delay(250);
}

// then check number of lives left
numLives -= 1;

if (numLives == 0) {
// if no more lives, blink red 3 times
for (int i = 0; i < 3; i += 1) {
blinkRed();
delay(250);
}
resetGame();
} else {
// reset player input pattern & position
resetArray(inputPattern);
patternPosition = 0;
// update lives
lightupLives();
}
} else if (patternPosition == patternLength) {
// if player has input all the pattern correctly
// celebrate!
for (int i = 0; i < 5; i += 1) {
runPurple();
}

// then reset game
resetGame();
}
}
}

void clearNeopixel() {
for (int i = 0; i < N_LEDS; i += 1) {
strip.setPixelColor(i, 0);
}

strip.show();
}

boolean checkArrayEmpty(int arr[]) {
boolean isEmpty = true;
for (int i = 0; i < patternLength; i += 1) {
if (arr[i] > 0) {
isEmpty = false;
}
}

return isEmpty;
}

// function to set up a pattern of random integers of 1, 2, 3
void setupPattern() {
// loop through pattern array, randomly set number and light up that LED
for (int i = 0; i < patternLength; i += 1) {
int ledIndex = random(0, 3072) / 1024;
pattern[i] = ledIndex;

Serial.print(ledIndex);
Serial.print("\t");
Serial.println(pattern[i]);

// light up LED
strip.setPixelColor(ledIndex + 1, ledColors[ledIndex]);
strip.show();
// wait 500ms, then turn it off
delay(500);
strip.setPixelColor(ledIndex + 1, 0); // turn off
strip.show();
delay(250);
}
}

// function to check if player input matches pattern
boolean checkPlayerPattern() {
boolean isMatch = true;
// loop through input pattern array, check if they match pattern array
for (int i = 0; i < patternPosition; i += 1) {
Serial.print(patternPosition);
Serial.print("\t");
Serial.print(pattern[i]);
Serial.print(" and ");
Serial.println(inputPattern[i]);
if (pattern[i] != inputPattern[i]) {
isMatch = false;
}
}

return isMatch;
}

void lightupLives() {
for (int i = 0; i < totalLives; i += 1) {
if (i < numLives) {
// light up that LED
strip.setPixelColor(i + 4, strip.Color(128, 0, 128)); // purple
} else {
// clear LED
strip.setPixelColor(i + 4, 0);
}
strip.show();
}
}

void blinkRed() {
// make all neopixels blink red
for (int i = 0; i < N_LEDS; i += 1) {
strip.setPixelColor(i, strip.Color(255, 0, 0));
}
strip.show();
delay(250);
for (int i = 0; i < N_LEDS; i += 1) {
strip.setPixelColor(i, 0);
}
strip.show();
}

void runPurple() {
for (int i = 0; i < N_LEDS + 4; i += 1) {
strip.setPixelColor(i, strip.Color(128, 0, 128)); // Draw new pixel
strip.setPixelColor(i - 4, 0); // Erase pixel a few steps back
strip.show();
delay(25);
}
}

void resetArray(int arr[]) {
for (int i = 0; i < patternLength; i += 1) {
arr[i] = 0;
}
}

// function to reset entire game
void resetGame() {
// set pattern and input array to empty
// set number of lives back to 2
// set button states all back to false
resetArray(pattern);
resetArray(inputPattern);
patternPosition = 0;
numLives = 3;

delay(1000);
}

Battery

With all the code working as they should, I thought switching our power source to a battery would be super easy. And oh boy was I so so wrong. But this is also where I had the greatest learnings.

When we switched to the battery, nothing happened. It turned out that the 9V from our battery was being converted to 5V through our regulator, then losing half of it because of the 10K ohm resistor. And supplying an Arduino Uno with only ~2V means that it wasn't even turning on.

The second issue we had was that because we were no longer plugged into a continous power source, we had to actually be aware of how much current each component needed. And apparently, each Neopixel requires somewhere between 60–80mAh of current—and because we had 12 Neopixels, that required 960mAh of current, which is huge.

As it turns out, an Arduino Uno actually requires 9V of power but also outputs 5V and 600mAh from the 5V pin. So Tom suggested we wire the battery to a toggle switch, then directly to the Uno so that it would get its 9V, and then from the Uno to the breadboard via the 5V pin. We also cut the Neopixels down to seven (560mAh) which was just barely under the 600mAh the Uno would provide.

When we made these changes, the Neopixel strip did light up:

Our circuit almost working as it should connected to the battery. Unfortunately not all Neopixels were lighting up as they should

But the Neopixel strip wasn't lighting up exactly as we were expecting. They would flicker, or miss a step, or freeze in the middle of a pattern. It turns out that the 9V battery Vesper had found on the junk shelf was actually at the end of its life and when we replaced it with a new, fully charged 9V battery, our whole circuit worked beautifully. So our final learning: even if all our math checks out, things in the physical world (unlike software and code) has a natural life span that degrades over time.

It really was a good thing that we made sure our code worked first, or else we would have been really confused by the way our Neopixel strip was working and thinking it might be because of our code.

So here is our final circuit, with the battery wiring updated and the pushbutton wiring simplified to take advantage of the Arduino's internal pullup resistors:

Assembly

The assembly was possibly the funnest part! Because we had gotten the battery working, we could fit everything into one box, so we started with a rough sketch of where we wanted all the components laid out:

We considered using a paper circuit so that we'd have more freedom in where we put the components, but ultimately decided that would be adding too much complexity. We realized that we could keep the whole thing quite compact if we stacked the breadboard on top of the Arduino and battery, and used some styrofoam for support.

We got feedback that styrofoam isn't the best material to use because it affects the wires, especially the wetter (?) styrofoam.

Jingjing found a website online where we could enter the dimensions of the box and it'd output a file for us to laser cut (she also found the cutest icons for the hearts and diamonds to decorate the box with). After two rounds of measuring and laser cutting, we had our enclosure:

Keeping the Neopixels in the correct place with (Pikachu hehe) tape.

We finished with some minimal labels of what the toggle switch and button on the left did by laser cutting some red acrylic we found in the scrap section of the shop, which Vesper painstakingly and meticulously glued onto our box:

All our parts–breadboard, Arduino, and battery—fitting beautifully into our box.