← Back to Portfolio

LCD Anniversary Quiz Game

Arduino-based quiz game with a 20×4 I²C LCD, four answer buttons, and a 24-LED heart driven by three daisy-chained 74HC595 shift registers made as a gift for my girlfriend

LCD Anniversary Quiz Game

Schematic

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.

Quiz Game Schematic

Prototype

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.

Assembly

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.

Final Assembly 1 Final Assembly 2 Final Assembly 3

Source Code

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.

Show Arduino Code
#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);
}

Final Product

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!