The first step was to create a schematic to remain organized while building the circuit, due to the large amount of connections needed in such a small space.
I first built the shift register circuit on a breadboard using multiple different colored LEDs I was considering using for the final product. I experimented with executing different patterns so that, once I built the lights into a heart shape, I could create an animation that is aesthetically appealing.
With my design verified, I could now begin transferring the circuit to a perfboard. I carefully used 30 AWG wire to make all the connections for the LED portion of the circuit and then 24 AWG for my powers, grounds, and I²C connections. Then I added a 9V battery socket and powered it up to make sure everything functioned correctly. Once I confirmed this, I sealed all the connections with UV resin and covered them with foam so the board could be held comfortably.
With all my connections made, it was time to create the code for the trivia quiz and integrate the LED animation to be triggered upon completion of the game. I first created a generic version with plans to go back and personalize it at the very end of the process. View inline below or download quiz_game.txt.
#include
#include
// I²C LCD: 20×4 display
LiquidCrystal_I2C lcd(0x27, 20, 4);
// Remapped button pins so A–D correspond to A3–A0 respectively
const int BTN_A = A3;
const int BTN_B = A2;
const int BTN_C = A1;
const int BTN_D = A0;
// Shift-register pins for LED animation
const int dataPin = 8;
const int latchPin = 12;
const int clockPin = 9;
// Game states
enum GameState { WELCOME, QUIZ };
GameState currentState = WELCOME;
// Blinking variables
unsigned long previousMillis = 0;
bool textVisible = true;
const int blinkInterval = 500; // ms
// Quiz variables
int currentQuestion = 0;
const int totalQuestions = 5;
// Helper to clear all LEDs
void clearLeds() {
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, 0x00);
shiftOut(dataPin, clockPin, MSBFIRST, 0x00);
shiftOut(dataPin, clockPin, MSBFIRST, 0x00);
digitalWrite(latchPin, HIGH);
}
void setup() {
// buttons
pinMode(BTN_A, INPUT_PULLUP);
pinMode(BTN_B, INPUT_PULLUP);
pinMode(BTN_C, INPUT_PULLUP);
pinMode(BTN_D, INPUT_PULLUP);
// LCD
Wire.begin();
lcd.init();
lcd.backlight();
// shift register outputs - initialize as early as possible
pinMode(dataPin, OUTPUT);
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
// Force all pins to known states immediately
digitalWrite(dataPin, LOW);
digitalWrite(clockPin, LOW);
digitalWrite(latchPin, LOW);
// Startup animation to take control of LEDs immediately
startupAnimation();
delay(500); // small startup delay
showWelcomeSequence();
}
void loop() {
unsigned long now = millis();
if (currentState == WELCOME && now - previousMillis >= blinkInterval) {
previousMillis = now;
textVisible = !textVisible;
if (textVisible) lcd.display();
else lcd.noDisplay();
}
if (currentState == WELCOME) {
handleWelcomeState();
} else {
handleQuizState();
}
}
void showWelcomeSequence() {
lcd.clear();
clearLeds(); // ensure LEDs stay off at start
lcd.setCursor(4, 0); lcd.print("Hey Claire!");
// LED fill animation during "Hey Claire!" message
for (int i = 0; i < 24; i++) {
uint32_t mask = 0;
// Light up all LEDs from 0 to current position
for (int j = 0; j <= i; j++) {
mask |= ((uint32_t)1 << j);
}
byte hb = (mask >> 16) & 0xFF;
byte mb = (mask >> 8) & 0xFF;
byte lb = mask & 0xFF;
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, hb);
shiftOut(dataPin, clockPin, MSBFIRST, mb);
shiftOut(dataPin, clockPin, MSBFIRST, lb);
digitalWrite(latchPin, HIGH);
delay(80); // ~2 seconds total for fill (24 * 80ms = 1920ms)
}
// Pause with all LEDs on for 1 second
delay(1000);
lcd.clear();
clearLeds(); // Turn off LEDs for quiz portion
lcd.setCursor(4, 0); lcd.print("Quiz time...");
delay(2000);
lcd.setCursor(3, 2); lcd.print("Are you ready?");
delay(2000);
lcd.clear();
lcd.setCursor(6, 0); lcd.print("If so...");
delay(1500);
lcd.setCursor(2, 2); lcd.print("Press any button");
// Flash the "Press any button" message
unsigned long flashStart = millis();
bool buttonPressed = false;
while (!buttonPressed) {
if (millis() - flashStart >= 500) { // Flash every 500ms
flashStart = millis();
textVisible = !textVisible;
if (textVisible) {
lcd.setCursor(2, 2); lcd.print("Press any button");
} else {
lcd.setCursor(2, 2); lcd.print(" "); // Clear the line
}
}
// Check for button press
if (!digitalRead(BTN_A) || !digitalRead(BTN_B) ||
!digitalRead(BTN_C) || !digitalRead(BTN_D)) {
buttonPressed = true;
}
}
lcd.clear();
lcd.setCursor(4, 0); lcd.print("Let's Begin!");
delay(1500);
currentState = QUIZ;
showQuestion();
}
void handleWelcomeState() {
// nothing here; button press handled above
}
void showQuestion() {
lcd.clear();
switch (currentQuestion) {
case 0:
lcd.setCursor(4,0); lcd.print("When is our");
lcd.setCursor(4,2); lcd.print("anniversary?");
delay(2000);
lcd.clear();
lcd.setCursor(1,0); lcd.print("A) 2/20 B) 2/22");
lcd.setCursor(1,2); lcd.print("C) 4/20 D) IDK");
break;
case 1:
lcd.setCursor(5,0); lcd.print("Where did we");
lcd.setCursor(5,2); lcd.print("first meet?");
delay(2000);
lcd.clear();
lcd.setCursor(5,0); lcd.print("A) Movegas");
lcd.setCursor(4,2); lcd.print("B) Rochester");
break;
case 2:
lcd.setCursor(4,0); lcd.print("What is my");
lcd.setCursor(4,2); lcd.print("middle name?");
delay(2000);
lcd.clear();
lcd.setCursor(1,0); lcd.print("A) Angus B) Arty");
lcd.setCursor(1,2); lcd.print("C) Andrew D) Bob");
break;
case 3:
lcd.setCursor(4,0); lcd.print("When is my");
lcd.setCursor(4,2); lcd.print("birthday?");
delay(2000);
lcd.clear();
lcd.setCursor(1,0); lcd.print("A) 9/17 B) 8/26");
lcd.setCursor(1,2); lcd.print("C) 12/5 D) 9/27");
break;
case 4:
lcd.setCursor(2,0); lcd.print("Who is the");
lcd.setCursor(2,2); lcd.print("best gf ever?");
delay(2000);
lcd.clear();
lcd.setCursor(0,0); lcd.print("A) Claire Holler");
lcd.setCursor(0,2); lcd.print("B) Anyone else");
break;
}
}
void handleQuizState() {
if (!digitalRead(BTN_A)) { processAnswer('A'); while (!digitalRead(BTN_A)); }
else if (!digitalRead(BTN_B)) { processAnswer('B'); while (!digitalRead(BTN_B)); }
else if (!digitalRead(BTN_C)) { processAnswer('C'); while (!digitalRead(BTN_C)); }
else if (!digitalRead(BTN_D)) { processAnswer('D'); while (!digitalRead(BTN_D)); }
}
void processAnswer(char answer) {
lcd.clear();
lcd.setCursor(0,0);
switch (currentQuestion) {
case 0:
if (answer == 'B') {
lcd.print("Correct!");
correctAnswerBlinks();
} else {
lcd.print("Bruh");
}
break;
case 1:
if (answer == 'A') {
lcd.print("Hell yeah!");
correctAnswerBlinks();
} else {
lcd.print("Awkward...");
}
break;
case 2:
if (answer == 'C') {
lcd.print("Easy!");
correctAnswerBlinks();
} else {
lcd.print("Ma'am...");
lcd.setCursor(0,2);
lcd.print("Are you high?");
}
break;
case 3:
if (answer == 'D') {
lcd.print("Bingo!");
correctAnswerBlinks();
delay(1000);
lcd.setCursor(0,2);
lcd.print("You rock <3");
} else {
lcd.print("Nice... lol");
}
break;
case 4:
if (answer == 'A') {
lcd.print("YUP!");
correctAnswerBlinks();
} else {
lcd.print("Absolutely!");
singleBlink(); // Single blink for the fake-out
delay(1000);
lcd.clear();
lcd.print("Jk lol...");
lcd.setCursor(0,2);
lcd.print("Ur the best");
}
break;
}
delay(2000);
currentQuestion++;
if (currentQuestion < totalQuestions) {
showQuestion();
} else {
// Quiz complete
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Quiz complete!");
delay(2000);
lcd.clear();
lcd.setCursor(3,1);
lcd.print("I love you");
// New animation sequence: 3 cycles of (circle + dancing)
for (int cycle = 0; cycle < 3; cycle++) {
// Circle animation (3 times)
for (int circleRound = 0; circleRound < 3; circleRound++) {
for (int i = 0; i < 24; i++) {
uint32_t mask = (uint32_t)1 << i;
byte hb = (mask >> 16) & 0xFF;
byte mb = (mask >> 8) & 0xFF;
byte lb = mask & 0xFF;
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, hb);
shiftOut(dataPin, clockPin, MSBFIRST, mb);
shiftOut(dataPin, clockPin, MSBFIRST, lb);
digitalWrite(latchPin, HIGH);
delay(33); // 50/1.5 ≈ 33ms for 1.5x speed
}
}
// Dancing animation - alternating odds/evens (3 times)
for (int danceRound = 0; danceRound < 3; danceRound++) {
// Show odd LEDs (bits 0, 2, 4, 6, ...)
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, 0xAA); // 10101010
shiftOut(dataPin, clockPin, MSBFIRST, 0xAA); // 10101010
shiftOut(dataPin, clockPin, MSBFIRST, 0xAA); // 10101010
digitalWrite(latchPin, HIGH);
delay(200); // 300/1.5 = 200ms for 1.5x speed
// Show even LEDs (bits 1, 3, 5, 7, ...)
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, 0x55); // 01010101
shiftOut(dataPin, clockPin, MSBFIRST, 0x55); // 01010101
shiftOut(dataPin, clockPin, MSBFIRST, 0x55); // 01010101
digitalWrite(latchPin, HIGH);
delay(200); // 300/1.5 = 200ms for 1.5x speed
}
}
// turn all LEDs off
clearLeds();
// prompt to play again with flashing text
lcd.clear();
lcd.setCursor((20-11)/2,0); lcd.print("Play again?");
// Flash the "Press any button" message
unsigned long flashStart = millis();
bool buttonPressed = false;
textVisible = true; // Reset text visibility
while (!buttonPressed) {
if (millis() - flashStart >= 500) { // Flash every 500ms
flashStart = millis();
textVisible = !textVisible;
if (textVisible) {
lcd.setCursor((20-16)/2,2); lcd.print("Press any button");
} else {
lcd.setCursor((20-16)/2,2); lcd.print(" "); // Clear the line
}
}
// Check for button press
if (!digitalRead(BTN_A) || !digitalRead(BTN_B) ||
!digitalRead(BTN_C) || !digitalRead(BTN_D)) {
buttonPressed = true;
}
}
// restart quiz
currentQuestion = 0;
currentState = WELCOME;
showWelcomeSequence();
}
}
void runLedAnimation() {
// unused
}
void startupAnimation() {
// Clear any random states immediately
clearLeds();
delay(50);
}
void correctAnswerBlinks() {
// 3 fast blinks on all LEDs
for (int blink = 0; blink < 3; blink++) {
// All LEDs on
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, 0xFF);
shiftOut(dataPin, clockPin, MSBFIRST, 0xFF);
shiftOut(dataPin, clockPin, MSBFIRST, 0xFF);
digitalWrite(latchPin, HIGH);
delay(150);
// All LEDs off
clearLeds();
delay(150);
}
}
void singleBlink() {
// Single blink for fake-out correct answer
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, 0xFF);
shiftOut(dataPin, clockPin, MSBFIRST, 0xFF);
shiftOut(dataPin, clockPin, MSBFIRST, 0xFF);
digitalWrite(latchPin, HIGH);
delay(150);
clearLeds();
delay(150);
}
Here is the generic version of the quiz played all the way through. This project was a huge challenge and learning experience on multiple levels. I had to use many resources to fill in the gaps within my programming knowledge to achieve the end product I wanted. My soldering skills were certainly tested and it took quite a bit of effort to come up with a strategy to correctly wire up so many connections right on top of each other. I was able to push through to create the final product I envisioned and create a great gift for my girlfriend!