This is a photo of my project currently running with the accessible button attached. The accessible button is the main controller of the game.
A birds-eye view of the project. Box measures around 10in by ~7in.
Currently, the person has 4 chances left to play the game! The LCD is inserted from the bottom up, so the end result is a very clean display.
The wood was laser cut to fit the servo from inside out also. It is secured using m3 screws.
The sensors detect if the popsicle stick (or anything) passes through them. They might not look aligned, but it still works!
Another example of a perfectly fit component. Please ignore the warped wood...
The player turns on the machine by pressing the black rotating button on the right. Once the stick is moving, the game is starting. Each time the stick passes the sensors, the machine beeps and also the ring changes color to green. If the player successfully presses the button when the ring is green, the player will increase a level. If the player incorrectly presses the button when the ring is not green, the player will lose a life with a starting amount of 5 lives. The player can change the speed of the stick moving with the rotating button the side. The white and red button can also be switched out to any button with a headphone jack.
For this warmup, I made a game inspired by a mobile-app game known as Pop the Lock. In that game, the objective is to tap the screen when the rotating line hits a randomly generated coin, and if you don't click it on time, you lose points. My game is a bit more simple, where there is only one target. You first insert your accessible button on the side, and then you press the rotary encoder to start the game. You can also turn the rotary encoder to increase the speed/difficulty of the game. When the stick hits the middle, the machine will output a sound, and the player must click their button exactly when the sound is played. The ring also changes from its rainbow color to green to indicate that you should press the button. If you fail to press it when the ring is green, you lose a life! You have 5 lives in total, and each time you successfully press the button, you increase one level! There is no prize for reaching the 10th and last level. "This game is hypnotically addictive!" - everyone.
This is the first ideation of what I wanted to create. The design stayed pretty true to the original plan.
Brought in all the required compenents and laying it out to see the actual size. It took a while to decide what other parts I wanted to add.
First time connecting all the parts to the Arduino Uno and programming it. Very messy! LCD didn't work multiple times...
Deciding placement of the parts on a cardboard prototype. Helped a lot to visualize the CAD (that my ex-partner did). I eventually moved the placement of the motor and ring, since there needed a place to put the sensors vertically.
Assembling the actual project using duct tape (and hope!). There is no bottom of this box, so I have to be very careful not to accidentally disconnect any wires, which happened often... Peep the screws on the LCD board and servo motor! Those are proudly not taped!
What was easy, what was hard, what did you learn? What little tweak, in retrospect, would’ve changed the direction entirely?
This was overall a very fun project to make. Thankfully, since we were able to use AI to help us write the code, debugging was no problem, which usually happens to be the most frustrating part of any code related project. While my partner left this class in the middle of the project due to scheduling reasons, I was very grateful that she had finished CADing this project, so I could lazer cut it without much difficulty (shoutout Wenyue). I am also thankful for Professor Zach for helping me tweak the CAD files, since I don't have experience using Fusion. I think lazer cutting and CADing was the biggest roadblock for me in this project just due to my lack of knowledge. It made me realize that this is something I needed to devolop my skills further, since it will become an important part of my projects in the future. I found the ideation part of this project the easiest. As soon as I read the write up, I knew instantly what kind of game I wanted to make. I guess I could've spent more time being open to other ideas and explore other designs, which would've obviously changed how the entire project looked. I think I am a bit disappointed that I was not able to make a complete enclosed box, due to my lack of CADing skills, and I also wished I could've added more decorative aspects to the box. Overall, this was a fun, little project, and I learned I need to desperately learn how to CAD. Thank you Wenyue and Prof Zach again for helping me with this project!
This code was prompted through ChatGPT. I don't have the exact prompt that I used to prompt the first iteration of the code, which lives in Wenyue's account. At the same time, most of the prompts I wrote to update the first iteration is filled with spelling and grammer errors, to the point where it could cause too much strain for the average person to read and decipher. Most of my conversation also just consisted of me feeding back error messages from the Arduino IDE. I am grateful that ChatGPT is trained on reading random strings of letters and parsing out what I want. Therefore, I asked ChatGPT to give me a condensed list of the questions and prompts I asked while iterating the code to its final form. This is what it said I asked.
I'm building an Arduino game using an Arduino Uno, rotary encoder, break-beam sensor, accessible push button connected through an audio jack, passive buzzer, NeoPixel LED ring, I²C LCD display, and servo motor.
I need the code to do the following:
The servo moves a stick back and forth, and the rotary encoder controls its speed.
Pressing the rotary encoder button starts or resets the game.
The break-beam sensor detects when the stick passes through it.
When the sensor detects the stick, the NeoPixel ring turns green and the buzzer makes a sound.
The green light should stay on a little longer after detection.
Pressing the accessible button while the ring is green counts as a correct press.
A correct press makes the ring green, plays a distinct buzzer sound, and increases the level by one.
An incorrect press makes the ring red, plays a different buzzer sound, and decreases the number of lives.
The buzzer should make different sounds for sensor detection, correct presses, and incorrect presses.
The NeoPixel ring should display a rainbow animation during normal gameplay.
The LCD should display the current level, remaining lives, and speed.
The game should have five lives and ten levels.
The code should use only the NeoPixel ring for LED feedback, not separate LEDs for individual lives.
I also need to create a schematic and functional block diagram for the project.
I asked for:
All the wiring connections between the Arduino and components.
The audio jack to be included because the accessible push button connects through it.
The input and output signals for the functional block diagram.
The actual signal names for the Arduino inputs.
The complete Arduino code, with all necessary functions included.
All unnecessary comments and excessive spacing removed, without removing any functional code.
Finally, I want the entire Arduino program in one complete, clean code block that I can copy into the Arduino IDE.
/*
* Arduino Pop-the-Lock Game
*
* This sketch controls an interactive reaction game using a servo,
* break-beam sensor, accessible push button, rotary encoder,
* NeoPixel ring, passive buzzer, and LCD display.
*
* The servo moves a stick back and forth across the break-beam
* sensor. When the stick is detected, the NeoPixel ring turns
* green and the player has a short period of time to press the
* accessible button. A correct press increases the level, while
* an incorrect press decreases the player's lives.
*
* The rotary encoder controls the speed of the servo, and pressing
* the encoder starts or resets the game. The LCD displays the
* current level, lives, and speed.
*
* Pin mapping:
*
* Arduino pin | role | details
* --------------------------------------------
* 2 | input | accessible push button
* 3 | input | rotary encoder CLK
* 4 | input | rotary encoder DT
* 5 | input | rotary encoder switch
* 6 | input | break-beam sensor
* 7 | output | passive buzzer
* 8 | output | servo motor
* 9 | output | NeoPixel LED ring
* A4 | I2C | LCD SDA
* A5 | I2C | LCD SCL
*/
#include <Servo.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>mm
#include <Adafruit_NeoPixel.h>
// Pin connections
const byte ACTION_BUTTON_PIN = 2;
const byte ENCODER_CLK_PIN = 3;
const byte ENCODER_DT_PIN = 4;
const byte ENCODER_SW_PIN = 5;
const byte BREAK_BEAM_PIN = 6;
const byte BUZZER_PIN = 7;
const byte SERVO_PIN = 8;
const byte NEOPIXEL_PIN = 9;
const byte NEOPIXEL_COUNT = 12;
LiquidCrystal_I2C lcd(0x27, 16, 2);
Adafruit_NeoPixel ring(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
Servo swingServo;
const byte SENSOR_DETECTED_LEVEL = LOW;
const byte MAX_LIVES = 5;
const byte MAX_LEVEL = 10;
byte lives = MAX_LIVES;
byte level = 1;
byte speedLevel = 5;
// Possible game states
enum GameState {
GAME_READY,
GAME_RUNNING,
GAME_FEEDBACK,
GAME_OVER,
GAME_WON
};
GameState gameState = GAME_READY;
int servoAngle = 0;
int servoDirection = 1;
bool servoAttached = false;
unsigned long previousServoTime = 0;
bool lastAttemptWasCorrect = false;
unsigned long feedbackStartTime = 0;
const unsigned long FEEDBACK_DURATION = 1000;
bool previousStickDetected = false;
bool greenWindowActive = false;
unsigned long greenWindowStartTime = 0;
const unsigned long GREEN_WINDOW_DURATION = 500;
bool actionStableState = HIGH;
bool actionLastReading = HIGH;
unsigned long actionChangeTime = 0;
bool encoderButtonStableState = HIGH;
bool encoderButtonLastReading = HIGH;
unsigned long encoderButtonChangeTime = 0;
const unsigned long DEBOUNCE_TIME = 30;
byte encoderHistory = 0;
int encoderAccumulator = 0;
const int8_t ENCODER_TABLE[16] = {
0, -1, 1, 0,
1, 0, 0, -1,
-1, 0, 0, 1,
0, 1, -1, 0
};
byte rainbowPosition = 0;
unsigned long previousRingTime = 0;
const unsigned long RING_INTERVAL = 40;
// Runs once when Arduino starts
void setup() {
pinMode(ACTION_BUTTON_PIN, INPUT_PULLUP);
pinMode(ENCODER_CLK_PIN, INPUT_PULLUP);
pinMode(ENCODER_DT_PIN, INPUT_PULLUP);
pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
pinMode(BREAK_BEAM_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
encoderHistory = (digitalRead(ENCODER_CLK_PIN) << 1) |
digitalRead(ENCODER_DT_PIN);
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("GAME READY");
lcd.setCursor(0, 1);
lcd.print("Press knob");
ring.begin();
ring.setBrightness(70);
clearRing();
noTone(BUZZER_PIN);
}
// Continuously runs the game
void loop() {
// Step 1: Read inputs
readActionButton();
readEncoderButton();
readRotaryEncoder();
updateSensor();
// Step 2: Make game decisions
updateFeedback();
// Step 3: Drive outputs
updateServo();
updateRingAnimation();
// Step 4: Information is reported through the LCD
}
// Read the main action button
void readActionButton() {
bool reading = digitalRead(ACTION_BUTTON_PIN);
if (reading != actionLastReading) {
actionChangeTime = millis();
}
if (millis() - actionChangeTime >= DEBOUNCE_TIME) {
if (reading != actionStableState) {
actionStableState = reading;
if (actionStableState == LOW) {
evaluateAttempt();
}
}
}
actionLastReading = reading;
}
// Check if the player pressed at the correct time
void evaluateAttempt() {
if (gameState != GAME_RUNNING) {
return;
}
bool correctPress = greenWindowActive;
lastAttemptWasCorrect = correctPress;
gameState = GAME_FEEDBACK;
feedbackStartTime = millis();
greenWindowActive = false;
if (correctPress) {
// Correct press
setRingColor(0, 255, 0);
tone(BUZZER_PIN, 1800, 250);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("TARGET HIT!");
lcd.setCursor(0, 1);
lcd.print("Level ");
lcd.print(level);
lcd.print(" clear");
}
else {
// Wrong press
if (lives > 0) {
lives--;
}
setRingColor(255, 0, 0);
tone(BUZZER_PIN, 500, 400);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MISSED!");
lcd.setCursor(0, 1);
lcd.print("Lives: ");
lcd.print(lives);
}
}
// Handle what happens after correct or wrong feedback
void updateFeedback() {
if (gameState != GAME_FEEDBACK) {
return;
}
if (millis() - feedbackStartTime < FEEDBACK_DURATION) {
return;
}
noTone(BUZZER_PIN);
if (lastAttemptWasCorrect) {
if (level >= MAX_LEVEL) {
showWinScreen();
}
else {
level++;
resumeGame();
}
}
else {
if (lives == 0) {
showGameOverScreen();
}
else {
resumeGame();
}
}
}
// Check if the stick breaks the beam
void updateSensor() {
bool stickDetected =
digitalRead(BREAK_BEAM_PIN) == SENSOR_DETECTED_LEVEL;
if (gameState != GAME_RUNNING) {
previousStickDetected = false;
return;
}
// Start green window when beam is broken
if (stickDetected && !previousStickDetected) {
greenWindowActive = true;
greenWindowStartTime = millis();
setRingColor(0, 255, 0);
tone(BUZZER_PIN, 1200, 80);
}
// End green window after 500 ms
if (greenWindowActive &&
millis() - greenWindowStartTime >= GREEN_WINDOW_DURATION) {
greenWindowActive = false;
clearRing();
}
previousStickDetected = stickDetected;
}
// Read the rotary encoder push button
void readEncoderButton() {
bool reading = digitalRead(ENCODER_SW_PIN);
if (reading != encoderButtonLastReading) {
encoderButtonChangeTime = millis();
}
if (millis() - encoderButtonChangeTime >= DEBOUNCE_TIME) {
if (reading != encoderButtonStableState) {
encoderButtonStableState = reading;
if (encoderButtonStableState == LOW) {
startNewGame();
}
}
}
encoderButtonLastReading = reading;
}
// Reset values and start a new game
void startNewGame() {
lives = MAX_LIVES;
level = 1;
speedLevel = 5;
servoAngle = 0;
servoDirection = 1;
previousStickDetected = false;
greenWindowActive = false;
lastAttemptWasCorrect = false;
noTone(BUZZER_PIN);
clearRing();
if (!servoAttached) {
swingServo.attach(SERVO_PIN);
servoAttached = true;
}
swingServo.write(servoAngle);
gameState = GAME_RUNNING;
previousServoTime = millis();
showRunningScreen();
}
// Resume game after feedback
void resumeGame() {
gameState = GAME_RUNNING;
previousServoTime = millis();
previousStickDetected = false;
greenWindowActive = false;
clearRing();
showRunningScreen();
}
// Show game over
void showGameOverScreen() {
gameState = GAME_OVER;
noTone(BUZZER_PIN);
setRingColor(255, 0, 0);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("GAME OVER");
lcd.setCursor(0, 1);
lcd.print("Press knob");
}
// Show win screen
void showWinScreen() {
gameState = GAME_WON;
noTone(BUZZER_PIN);
setRingColor(0, 255, 0);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("YOU WIN!");
lcd.setCursor(0, 1);
lcd.print("Press knob");
}
// Determine servo speed
unsigned long getServoInterval() {
return map(speedLevel, 1, 10, 35, 3);
}
// Move servo back and forth
void updateServo() {
if (gameState != GAME_RUNNING || !servoAttached) {
return;
}
unsigned long servoInterval = getServoInterval();
if (millis() - previousServoTime >= servoInterval) {
previousServoTime = millis();
servoAngle += servoDirection;
if (servoAngle >= 180) {
servoAngle = 180;
servoDirection = -1;
}
else if (servoAngle <= 0) {
servoAngle = 0;
servoDirection = 1;
}
swingServo.write(servoAngle);
}
}
// Read rotary encoder movement
void readRotaryEncoder() {
byte currentState = (digitalRead(ENCODER_CLK_PIN) << 1) |
digitalRead(ENCODER_DT_PIN);
encoderHistory = ((encoderHistory << 2) | currentState) & 0x0F;
int movement = ENCODER_TABLE[encoderHistory];
if (movement == 0) {
return;
}
encoderAccumulator += movement;
if (encoderAccumulator >= 4) {
encoderAccumulator = 0;
changeSpeed(1);
}
else if (encoderAccumulator <= -4) {
encoderAccumulator = 0;
changeSpeed(-1);
}
}
// Change servo speed
void changeSpeed(int amount) {
if (gameState != GAME_RUNNING) {
return;
}
int newSpeed = speedLevel + amount;
speedLevel = constrain(newSpeed, 1, 10);
showRunningScreen();
}
// Show level, lives, and speed
void showRunningScreen() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Level:");
lcd.print(level);
lcd.print("/");
lcd.print(MAX_LEVEL);
lcd.setCursor(0, 1);
lcd.print("Lives:");
lcd.print(lives);
lcd.print(" Spd:");
lcd.print(speedLevel);
}
// Rainbow LED animation
void updateRingAnimation() {
if (gameState != GAME_RUNNING) {
return;
}
if (greenWindowActive) {
return;
}
if (millis() - previousRingTime < RING_INTERVAL) {
return;
}
previousRingTime = millis();
for (byte i = 0; i < NEOPIXEL_COUNT; i++) {
byte position =
rainbowPosition + ((i * 256) / NEOPIXEL_COUNT);
ring.setPixelColor(i, colorWheel(position));
}
ring.show();
rainbowPosition += 2;
}
// Generate rainbow colors
uint32_t colorWheel(byte position) {
position = 255 - position;
if (position < 85) {
return ring.Color(
255 - position * 3,
0,
position * 3
);
}
if (position < 170) {
position -= 85;
return ring.Color(
0,
position * 3,
255 - position * 3
);
}
position -= 170;
return ring.Color(
position * 3,
255 - position * 3,
0
);
}
// Set entire ring to one color
void setRingColor(byte red, byte green, byte blue) {
for (byte i = 0; i < NEOPIXEL_COUNT; i++) {
ring.setPixelColor(i, ring.Color(red, green, blue));
}
ring.show();
}
// Turn off the LED ring
void clearRing() {
ring.clear();
ring.show();
}