It seems to also be a combined program, showing unused remnant code.
click for sketch.ino
// https://forum.arduino.cc/t/project-pinball-redesign-problem-with-triggering-bumpers/1389678/3
// 1 = bumper4
// 2 = Ramp6
// 3 = Ramp 4
// 4 = hit target3
// 5 = looseBall
// 6 = objective
// 7= ramp 11
// 8 = intro
// 9= ramp9
// 13 = intro space!
// 16 = triple kill
// 18 = Relaunch
// 19 = upgrade complete
// 20 = highscore
// 21 = personal highscore
// 22 = end of game
// 23 = double kill
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <Encoder.h>
#include <FastLED.h>
#include <DFRobotDFPlayerMini.h>
#define DF_RX 16 // to module TX
#define DF_TX 17 // to module RX
// Rotary Encoder Pins
#define ENCODER_PIN_A 18
#define ENCODER_PIN_B 19
#define ENCODER_BUTTON_PIN 24
// Game Buttons
#define BUTTON_SUB_BALL_PIN 52
// Objective Leds
#define OBJ_RAMP3A_LED_PIN 34
#define OBJ_RAMP3B_LED_PIN 35
#define OBJ_RAMP2A_LED_PIN 36
#define OBJ_RAMP2B_LED_PIN 37
#define OBJ_RAMP1A_LED_PIN 38
#define OBJ_RAMP1B_LED_PIN 39
#define OBJ_TUNNELA_LED_PIN 40
#define OBJ_TUNNELB_LED_PIN 41
#define OBJ_HITTARGET_LED_PIN 42
// Ramp and tunnel leds.
#define RAMP1_LED_PIN 44
#define RAMP2_LED_PIN 45
#define RAMP3_LED_PIN 46
#define TUNNEL_LED_PIN 49
#define HITTARGET_LED_PIN 48
//other leds
#define SAVEBALL_LED_PIN 33
// hit target
#define HIT_TARGET_PIN A0
const int hitTargetThreshold = 150;
bool lastHitState = false;
// Ramps
#define RAMP3_SENSOR_PIN A2
const int ramp3Threshold = 100; // your chosen threshold
bool lastRamp3Hit = false;
#define RAMP2_SENSOR_PIN A3
const int ramp2Threshold = 100; // your chosen threshold
bool lastRamp2Hit = false;
#define RAMP1_SENSOR_PIN A4
const int ramp1Threshold = 100; // your chosen threshold
bool lastRamp1Hit = false;
#define TUNNEL_SENSOR_PIN A5
const int tunnelThreshold = 100; // your chosen threshold
bool lastTunnelHit = false;
//objective counters
unsigned int ramp3Counter = 0;
unsigned int ramp2Counter = 0;
unsigned int ramp1Counter = 0;
unsigned int tunnelCounter = 0;
unsigned int bumperCounter = 0;
unsigned int hitTargetCounter = 0;
//Scoring Factors
float Ramp1Factor = 1;
float Ramp2Factor = 1;
float Ramp3Factor = 1;
float TunnelFactor = 1;
float BumperFactor = 1;
float HitTargetFactor = 1;
//Save ball tracker
unsigned int SaveBallScore = 0;
LiquidCrystal_I2C lcd(0x27, 16, 2); // LCD address and dimensions
Encoder encoder(ENCODER_PIN_A, ENCODER_PIN_B); // Using Encoder library
#define EEPROM_VERSION 0x42
#define PLAYER_START_ADDR 10
#define MAX_PLAYERS 70
#define STRIP_TOP_PIN 6
#define STRIP_TOP_NUM_LEDS 12
#define STRIP_TOP_BRIGHTNESS 255 // 100
#define STRIP_TOP_TYPE WS2812B
#define STRIP_TOP_ORDER GRB
CRGB stripTopLeds[STRIP_TOP_NUM_LEDS];
bool lastSubBallButtonState = LOW;
DFRobotDFPlayerMini dfplayer;
//combo variables
bool comboActive = false; // are we in a combo?
uint8_t comboHits = 0; // how many bonus hits so far (0–3)
unsigned long comboStart = 0; // timestamp of last ramp in combo
const unsigned long comboWindow = 10000; // 10 s window
//structuring a player
struct Player {
char name[4]; // 3 chars + null
uint32_t score;
};
int playerCount = 0;
int encoderPos = 0;
int lastEncoderPos = -999;
int hsStatus = 1;
const char allowedChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
const int numChars = sizeof(allowedChars) - 1; // exclude terminating '\0'
bool ramp2EffectActive = false;
unsigned long ramp2EffectStartTime = 0;
void setup() {
Serial.begin(9600);
Serial2.begin(9600);
lcd.init();
lcd.backlight();
pinMode(ENCODER_BUTTON_PIN, INPUT_PULLUP);
pinMode(BUTTON_SUB_BALL_PIN, INPUT);
initEEPROM();
playerCount = getPlayerCount();
// if (!dfplayer.begin(Serial2)) {
// while (1);
// }
dfplayer.begin(Serial2);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Button to Start");
lcd.setCursor(0, 1);
lcd.print("HS");
printScoreRightAligned(getHighScore(), 1);
lastRamp2Hit = (analogRead(RAMP2_SENSOR_PIN) > ramp2Threshold);
lastRamp3Hit = (analogRead(RAMP3_SENSOR_PIN) > ramp3Threshold);
lastRamp1Hit = (analogRead(RAMP1_SENSOR_PIN) > ramp1Threshold);
lastTunnelHit = (analogRead(TUNNEL_SENSOR_PIN) > tunnelThreshold);
lastHitState = (analogRead(HIT_TARGET_PIN) >= hitTargetThreshold);
pinMode(OBJ_RAMP3A_LED_PIN, OUTPUT);
pinMode(OBJ_RAMP3B_LED_PIN, OUTPUT);
pinMode(OBJ_RAMP2A_LED_PIN, OUTPUT);
pinMode(OBJ_RAMP2B_LED_PIN, OUTPUT);
pinMode(OBJ_RAMP1A_LED_PIN, OUTPUT);
pinMode(OBJ_RAMP1B_LED_PIN, OUTPUT);
pinMode(OBJ_TUNNELA_LED_PIN, OUTPUT);
pinMode(OBJ_TUNNELB_LED_PIN, OUTPUT);
pinMode(OBJ_HITTARGET_LED_PIN, OUTPUT);
pinMode(RAMP1_LED_PIN, OUTPUT);
pinMode(RAMP2_LED_PIN, OUTPUT);
pinMode(RAMP3_LED_PIN, OUTPUT);
pinMode(TUNNEL_LED_PIN, OUTPUT);
pinMode(HITTARGET_LED_PIN, OUTPUT);
pinMode(SAVEBALL_LED_PIN, OUTPUT);
pinMode(HIT_TARGET_PIN, INPUT);
FastLED.addLeds<STRIP_TOP_TYPE, STRIP_TOP_PIN, STRIP_TOP_ORDER>(stripTopLeds, STRIP_TOP_NUM_LEDS);
FastLED.setBrightness(STRIP_TOP_BRIGHTNESS);
}
void loop() {
if (digitalRead(ENCODER_BUTTON_PIN) == LOW) {
delay(200);
while (digitalRead(ENCODER_BUTTON_PIN) == LOW);
dfplayer.volume(25);
dfplayer.play(13);
showPlayerMenu();
}
}
void showPlayerMenu() {
fill_solid(stripTopLeds, STRIP_TOP_NUM_LEDS, CHSV(96, 200, 255)); // light blue hue
FastLED.show();
encoder.write(0);
encoderPos = 0;
lastEncoderPos = -999;
while (true) {
encoderPos = encoder.read() / 4; // Reduce sensitivity
if (encoderPos < 0) encoderPos = playerCount;
if (encoderPos > playerCount) encoderPos = 0;
if (lastEncoderPos != encoderPos) {
lastEncoderPos = encoderPos;
lcd.clear();
if (encoderPos == 0) {
lcd.setCursor(0, 0);
lcd.print("> New Player");
if (playerCount > 0) {
Player p = readPlayer(0);
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.print(p.name);
lcd.print(" - ");
lcd.print(p.score);
}
} else {
int firstIndex = encoderPos - 1;
Player p1 = readPlayer(firstIndex);
lcd.setCursor(0, 0);
lcd.print("> ");
lcd.print(p1.name);
lcd.print(" - ");
lcd.print(p1.score);
lcd.setCursor(0, 1);
if (firstIndex + 1 < playerCount) {
Player p2 = readPlayer(firstIndex + 1);
lcd.print(" ");
lcd.print(p2.name);
lcd.print(" - ");
lcd.print(p2.score);
} else {
lcd.print(" ");
}
}
}
if (digitalRead(ENCODER_BUTTON_PIN) == LOW) {
delay(200);
while (digitalRead(ENCODER_BUTTON_PIN) == LOW);
if (encoderPos == 0) {
enterNewPlayer();
playerCount = getPlayerCount();
encoder.write(0);
lastEncoderPos = -999;
} else {
Player p = readPlayer(encoderPos - 1);
bool start = confirmPlayerSelection(p);
if (start) {
dfplayer.volume(25);
dfplayer.play(7);
playStripTopParade();
startGame(p);
break; // Return to main menu after game screen
} else {
showCancelMessageAndReturn();
encoder.write(0);
lastEncoderPos = -999;
}
}
}
}
}
void showCancelMessageAndReturn() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Cancel, back to");
lcd.setCursor(0, 1);
lcd.print("player selection");
delay(1500);
}
bool confirmPlayerSelection(Player p) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Start game as:");
lcd.setCursor(0, 1);
lcd.print(p.name);
unsigned long pressStart = 0;
bool buttonPressed = false;
while (true) {
int buttonState = digitalRead(ENCODER_BUTTON_PIN);
if (buttonState == LOW && !buttonPressed) {
buttonPressed = true;
pressStart = millis();
} else if (buttonState == LOW && buttonPressed) {
unsigned long pressDuration = millis() - pressStart;
if (pressDuration >= 1000) {
showCancelMessageAndReturn();
while (digitalRead(ENCODER_BUTTON_PIN) == LOW) {
delay(10);
}
delay(100);
return false;
}
} else if (buttonState == HIGH && buttonPressed) {
return true;
}
delay(10);
}
}
void startGame(Player p) {
lcd.clear();
int RemainingBalls = 3;
int CurrentScore = 0;
// Show player name left aligned on second row
lcd.setCursor(0, 1);
lcd.print(p.name);
// Show personal best right aligned on second row
printScoreRightAligned(p.score, 1);
updateScreen(RemainingBalls, CurrentScore, p);
while (RemainingBalls > 0) {
// expire combo if time’s up
if (comboActive && millis() - comboStart > comboWindow) {
comboActive = false;
comboHits = 0;
digitalWrite(RAMP1_LED_PIN, LOW);
digitalWrite(RAMP2_LED_PIN, LOW);
digitalWrite(RAMP3_LED_PIN, LOW);
digitalWrite(TUNNEL_LED_PIN, LOW);
}
updateRamp2Effect(); // Run animation logic continuously
Ramp1Trigger(RemainingBalls, CurrentScore, p);
Ramp2Trigger(RemainingBalls, CurrentScore, p);
Ramp3Trigger(RemainingBalls, CurrentScore, p);
TunnelTrigger(RemainingBalls, CurrentScore, p);
HitTargetTrigger(RemainingBalls, CurrentScore, p);
SubtractBall (RemainingBalls, CurrentScore, p);
CheckObjectives (RemainingBalls, CurrentScore, p);
CheckSaveBall (CurrentScore);
}
}
void CheckSaveBall(int &CurrentScore) {
if (CurrentScore == SaveBallScore) {
digitalWrite(SAVEBALL_LED_PIN, HIGH);
} else {
digitalWrite(SAVEBALL_LED_PIN, LOW);
}
}
void updateScreen(int RemainingBalls, int CurrentScore, Player p) {
// Prevent LCD overflow
if (RemainingBalls > 5) RemainingBalls = 5;
lcd.setCursor(0, 0);
lcd.print(" "); // Clear first row
lcd.setCursor(0, 0);
for (int i = 0; i < RemainingBalls; i++) {
lcd.print('O');
}
String scoreStr = String(CurrentScore);
int len = scoreStr.length();
int startPos = 16 - len;
lcd.setCursor(startPos, 0);
lcd.print(scoreStr);
if (CurrentScore >= p.score && hsStatus == 1) {
hsStatus = 2;
}
if (hsStatus == 2) {
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("HS");
printScoreRightAligned(getHighScore(), 1);
hsStatus = 3;
dfplayer.volume(25);
dfplayer.play(21);
}
else if (CurrentScore > getHighScore() && hsStatus == 3) {
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(" NEW HIGH SCORE ");
hsStatus = 4;
dfplayer.volume(25);
dfplayer.play(20);
}
}
void enterNewPlayer() {
char name[4] = {'A', 'A', 'A', '\0'};
int letterIndex = 0;
encoder.write(0);
encoderPos = 0;
lastEncoderPos = -999;
unsigned long pressStart = 0;
bool buttonPressed = false;
while (letterIndex < 3) {
encoderPos = encoder.read() / 4;
if (encoderPos < 0) encoderPos = numChars - 1;
if (encoderPos >= numChars) encoderPos = 0;
if (lastEncoderPos != encoderPos) {
lastEncoderPos = encoderPos;
name[letterIndex] = allowedChars[encoderPos];
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enter Name:");
lcd.setCursor(0, 1);
for (int i = 0; i < 3; i++) {
if (i == letterIndex) lcd.print('[');
else lcd.print(' ');
lcd.print(name[i]);
if (i == letterIndex) lcd.print(']');
else lcd.print(' ');
}
}
int buttonState = digitalRead(ENCODER_BUTTON_PIN);
if (buttonState == LOW && !buttonPressed) {
buttonPressed = true;
pressStart = millis();
} else if (buttonState == LOW && buttonPressed) {
unsigned long pressDuration = millis() - pressStart;
if (pressDuration >= 1000) {
showCancelMessageAndReturn();
while (digitalRead(ENCODER_BUTTON_PIN) == LOW) {
delay(10);
}
delay(100);
return;
}
} else if (buttonState == HIGH && buttonPressed) {
buttonPressed = false;
letterIndex++;
encoder.write(0);
lastEncoderPos = -999;
}
delay(10);
}
bool exists = false;
for (int i = 0; i < playerCount; i++) {
Player p = readPlayer(i);
if (strcmp(p.name, name) == 0) {
exists = true;
break;
}
}
if (!exists && playerCount < MAX_PLAYERS) {
Player newPlayer;
strcpy(newPlayer.name, name);
newPlayer.score = 0;
savePlayer(playerCount, newPlayer);
playerCount++;
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Name Exists!");
delay(2000);
}
}
void initEEPROM() {
if (EEPROM.read(0) != EEPROM_VERSION) {
EEPROM.write(0, EEPROM_VERSION);
EEPROM.write(1, 0); // player count
}
playerCount = getPlayerCount(); // ensure global stays in sync
}
int getPlayerCount() {
return EEPROM.read(1);
}
void savePlayer(int index, Player p) {
int addr = PLAYER_START_ADDR + index * sizeof(Player);
EEPROM.put(addr, p);
// Only update count if adding a new player
if (index >= EEPROM.read(1)) {
EEPROM.write(1, index + 1);
}
sortPlayersAlphabetically();
}
Player readPlayer(int index) {
Player p;
int addr = PLAYER_START_ADDR + index * sizeof(Player);
EEPROM.get(addr, p);
return p;
}
void sortPlayersAlphabetically() {
if (playerCount < 2) return;
for (int i = 0; i < playerCount - 1; i++) {
for (int j = 0; j < playerCount - i - 1; j++) {
Player p1 = readPlayer(j);
Player p2 = readPlayer(j + 1);
if (strcmp(p1.name, p2.name) > 0) {
savePlayerRaw(j, p2);
savePlayerRaw(j + 1, p1);
}
}
}
}
void savePlayerRaw(int index, Player p) {
int addr = PLAYER_START_ADDR + index * sizeof(Player);
EEPROM.put(addr, p);
}
int findPlayerIndex(const char *name) {
for (int i = 0; i < playerCount; i++) {
Player p = readPlayer(i);
if (strcmp(p.name, name) == 0) return i;
}
return -1;
}
uint32_t getHighScore() {
uint32_t hs = 0;
for (int i = 0; i < playerCount; i++) {
Player p = readPlayer(i);
if (p.score > hs) hs = p.score;
}
return hs;
}
void printScoreRightAligned(uint32_t score, int row) {
String scoreStr = String(score);
int len = scoreStr.length();
int startPos = 16 - len;
lcd.setCursor(startPos, row);
lcd.print(scoreStr);
for (int i = 0; i < startPos; i++) {
lcd.print(" ");
}
}
void playStripTopParade() {
unsigned long start = millis();
uint8_t hue = 0;
while (millis() - start < 3000) {
for (int i = 0; i < STRIP_TOP_NUM_LEDS; i++) {
stripTopLeds[i] = CHSV(hue + (i * 10), 255, 255);
}
FastLED.show();
hue += 5;
delay(50);
}
fill_solid(stripTopLeds, STRIP_TOP_NUM_LEDS, CHSV(192, 255, 255)); // Purple hue
FastLED.show();
}
void HitTargetTrigger(int &RemainingBalls, int &CurrentScore, Player &p) {
analogRead(HIT_TARGET_PIN);
// 1) read sensor and compare to threshold
int v = analogRead(HIT_TARGET_PIN);
bool isHit = (v >= hitTargetThreshold);
// 2) rising‐edge: not hit → hit
if (isHit && !lastHitState) {
hitTargetCounter++;
const uint16_t pts = 30;
CurrentScore += pts;
// play your “hit target” sound (track 4)
dfplayer.volume(25);
dfplayer.play(4);
// update the display
updateScreen(RemainingBalls, CurrentScore, p);
// (optional) flash an LED
//digitalWrite(HITTARGET_LED_PIN, HIGH);
//delay(50);
//digitalWrite(HITTARGET_LED_PIN, LOW);
//delay(50);
//digitalWrite(HITTARGET_LED_PIN, HIGH);
//delay(50);
//digitalWrite(HITTARGET_LED_PIN, LOW);
//delay(50);
//digitalWrite(HITTARGET_LED_PIN, HIGH);
//delay(50);
// digitalWrite(HITTARGET_LED_PIN, LOW);
}
// 3) remember state for next loop
lastHitState = isHit;
}
void Ramp1Trigger(int &RemainingBalls, int &CurrentScore, Player &p) {
int raw = analogRead(RAMP1_SENSOR_PIN);
// Determine if object is close enough
bool isClose = (raw > ramp1Threshold);
// Rising-edge: far → close
if (isClose && !lastRamp1Hit) {
uint16_t points = round(150 * Ramp1Factor);
CurrentScore += points;
dfplayer.volume(25);
dfplayer.play(7);
updateScreen(RemainingBalls, CurrentScore, p);
ramp1Counter++;
handleComboTimer(RemainingBalls, CurrentScore, p, points);
}
lastRamp1Hit = isClose;
}
void Ramp2Trigger(int &RemainingBalls, int &CurrentScore, Player &p) {
int raw = analogRead(RAMP2_SENSOR_PIN);
// Determine if object is close enough
bool isClose = (raw > ramp2Threshold);
// Rising-edge: far → close
if (isClose && !lastRamp2Hit) {
uint16_t points = round(250 * Ramp2Factor);
CurrentScore += points;
dfplayer.volume(25);
dfplayer.play(2);
updateScreen(RemainingBalls, CurrentScore, p);
ramp2Counter++;
handleComboTimer(RemainingBalls, CurrentScore, p, points);
// Trigger the Ramp2 light effect
ramp2EffectActive = true;
ramp2EffectStartTime = millis();
}
lastRamp2Hit = isClose;
}
void Ramp3Trigger(int &RemainingBalls, int &CurrentScore, Player &p) {
int raw = analogRead(RAMP3_SENSOR_PIN);
// Determine if object is close enough
bool isClose = (raw > ramp3Threshold);
// Rising-edge: far → close
if (isClose && !lastRamp3Hit) {
uint16_t points = round(120 * Ramp3Factor);
CurrentScore += points;
dfplayer.volume(25);
dfplayer.play(9);
updateScreen(RemainingBalls, CurrentScore, p);
ramp3Counter++;
handleComboTimer(RemainingBalls, CurrentScore, p, points);
}
lastRamp3Hit = isClose;
}
void TunnelTrigger(int &RemainingBalls, int &CurrentScore, Player &p) {
int raw = analogRead(TUNNEL_SENSOR_PIN);
// Determine if object is close enough
bool isClose = (raw > tunnelThreshold);
// Rising-edge: far → close
if (isClose && !lastTunnelHit) {
uint16_t points = round(200 * TunnelFactor);
CurrentScore += points;
dfplayer.volume(25);
dfplayer.play(3);
updateScreen(RemainingBalls, CurrentScore, p);
tunnelCounter++;
handleComboTimer(RemainingBalls, CurrentScore, p, points);
}
lastTunnelHit = isClose;
}
void CheckObjectives (int &RemainingBalls, int &CurrentScore, Player &p) {
if (ramp3Counter == 5) {
digitalWrite(OBJ_RAMP3A_LED_PIN, HIGH);
CurrentScore += 500;
ramp3Counter++;
Ramp3Factor = 1.2 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (ramp3Counter == 11) {
digitalWrite(OBJ_RAMP3B_LED_PIN, HIGH);
CurrentScore += 1200;
ramp3Counter++;
Ramp3Factor = 1.5 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (ramp2Counter == 3) {
digitalWrite(OBJ_RAMP2A_LED_PIN, HIGH);
CurrentScore += 1000;
ramp2Counter++;
Ramp2Factor = 1.2 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (ramp2Counter == 7) {
digitalWrite(OBJ_RAMP2B_LED_PIN, HIGH);
CurrentScore += 2500;
ramp2Counter++;
Ramp2Factor = 1.5 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (ramp1Counter == 4) {
digitalWrite(OBJ_RAMP1A_LED_PIN, HIGH);
CurrentScore += 700;
ramp1Counter++;
Ramp1Factor = 1.2 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (ramp1Counter == 9) {
digitalWrite(OBJ_RAMP1B_LED_PIN, HIGH);
CurrentScore += 1800;
ramp1Counter++;
Ramp1Factor = 1.5 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (tunnelCounter == 3) {
digitalWrite(OBJ_TUNNELA_LED_PIN, HIGH);
CurrentScore += 900;
tunnelCounter++;
TunnelFactor = 1.2 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (tunnelCounter == 7) {
digitalWrite(OBJ_TUNNELB_LED_PIN, HIGH);
CurrentScore += 2000;
tunnelCounter++;
TunnelFactor = 1.5 ;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
if (hitTargetCounter == 20) {
digitalWrite(OBJ_HITTARGET_LED_PIN, HIGH);
CurrentScore += 2000;
HitTargetFactor = 2 ;
hitTargetCounter++;
dfplayer.volume(25);
dfplayer.play(19);
updateScreen(RemainingBalls, CurrentScore, p);
}
}
void handleComboTimer(int RemainingBalls, int &CurrentScore, Player p, uint16_t &points) {
unsigned long now = millis();
if (!comboActive) {
comboActive = true;
comboHits = 1;
comboStart = now;
digitalWrite(RAMP1_LED_PIN, HIGH);
digitalWrite(RAMP2_LED_PIN, HIGH);
digitalWrite(RAMP3_LED_PIN, HIGH);
digitalWrite(TUNNEL_LED_PIN, HIGH);
}
else {
if (now - comboStart <= comboWindow && comboHits < 3) {
comboHits++;
// Play different sounds based on double vs. triple
if (comboHits == 2) {
CurrentScore += points;
dfplayer.volume(25);
dfplayer.play(23); // double-hit sound
updateScreen(RemainingBalls, CurrentScore, p);
}
if (comboHits == 3) {
CurrentScore += 2 * points;
dfplayer.volume(25);
dfplayer.play(16); // triple-hit sound
updateScreen(RemainingBalls, CurrentScore, p);
digitalWrite(RAMP1_LED_PIN, LOW);
digitalWrite(RAMP2_LED_PIN, LOW);
digitalWrite(RAMP3_LED_PIN, LOW);
digitalWrite(TUNNEL_LED_PIN, LOW);
}
// Continue the timer only if you might get a next hit
if (comboHits < 3) {
comboStart = now;
} else {
comboActive = false; // combo completed
}
}
else {
// expired or already maxed → start fresh
comboHits = 1;
comboStart = now;
digitalWrite(RAMP1_LED_PIN, HIGH);
digitalWrite(RAMP2_LED_PIN, HIGH);
digitalWrite(RAMP3_LED_PIN, HIGH);
digitalWrite(TUNNEL_LED_PIN, HIGH);
}
}
}
void SubtractBall (int &RemainingBalls, int &CurrentScore, Player &p) {
bool subBallButtonState = digitalRead(BUTTON_SUB_BALL_PIN);
// Detect rising edge for Subtract Ball button
if (subBallButtonState == HIGH && lastSubBallButtonState == LOW) {
if (CurrentScore == SaveBallScore) {
dfplayer.volume(25);
dfplayer.play(18);
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print(" BALL SAVED! ");
delay(3000);
updateScreen(RemainingBalls, CurrentScore, p);
return;
}
if (RemainingBalls > 0) {
dfplayer.volume(28);
dfplayer.play(5);
RemainingBalls--;
SaveBallScore = CurrentScore;
updateScreen(RemainingBalls, CurrentScore, p);
delay(2000);
}
}
lastSubBallButtonState = subBallButtonState;
if (RemainingBalls == 0) {
dfplayer.volume(25);
dfplayer.play(22);
// Game Over Screen
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" GAME OVER! ");
lcd.setCursor(0, 1);
if (CurrentScore > p.score) {
lcd.print(" Score: ");
lcd.print(CurrentScore);
p.score = CurrentScore;
savePlayer(findPlayerIndex(p.name), p);
playStripTopParade();
} else {
lcd.print(" Score: ");
lcd.print(CurrentScore);
playStripTopParade();
}
// Wait for rotary encoder button press to restart
while (digitalRead(ENCODER_BUTTON_PIN) == HIGH) {
delay(50);
}
delay(200); // debounce
while (digitalRead(ENCODER_BUTTON_PIN) == LOW) {
delay(50);
}
delay(200);
hsStatus = 1;
digitalWrite(OBJ_RAMP3A_LED_PIN, LOW);
digitalWrite(OBJ_RAMP3B_LED_PIN, LOW);
digitalWrite(OBJ_RAMP2A_LED_PIN, LOW);
digitalWrite(OBJ_RAMP2B_LED_PIN, LOW);
digitalWrite(OBJ_RAMP1A_LED_PIN, LOW);
digitalWrite(OBJ_RAMP1B_LED_PIN, LOW);
digitalWrite(OBJ_TUNNELA_LED_PIN, LOW);
digitalWrite(OBJ_TUNNELB_LED_PIN, LOW);
digitalWrite(RAMP1_LED_PIN, LOW);
digitalWrite(RAMP2_LED_PIN, LOW);
digitalWrite(RAMP3_LED_PIN, LOW);
digitalWrite(TUNNEL_LED_PIN, LOW);
digitalWrite(SAVEBALL_LED_PIN, LOW);
ramp3Counter = 0;
ramp2Counter = 0;
ramp1Counter = 0;
tunnelCounter = 0;
tunnelCounter = 0;
bumperCounter = 0;
hitTargetCounter = 0;
SaveBallScore = 0;
Ramp1Factor = 1;
Ramp2Factor = 1;
Ramp3Factor = 1;
TunnelFactor = 1;
BumperFactor = 1;
HitTargetFactor = 1;
return; // Back to player menu
}
delay(50); // debounce & CPU relief
}
void updateRamp2Effect() {
if (!ramp2EffectActive) return;
unsigned long elapsed = millis() - ramp2EffectStartTime;
const unsigned long duration = 1000; // 1 second animation
if (elapsed > duration) {
ramp2EffectActive = false;
fill_solid(stripTopLeds, STRIP_TOP_NUM_LEDS, CHSV(192, 255, 255)); // Default color
FastLED.show();
return;
}
// Simple pulsing effect
uint8_t brightness = 100 + 100 * sin(PI * elapsed / (float)duration);
fill_solid(stripTopLeds, STRIP_TOP_NUM_LEDS, CHSV(32, 255, brightness)); // Orange pulse
FastLED.show();
}
click for diagram.json
{
"version": 1,
"author": "foreignpigdog x",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-arduino-mega", "id": "mega", "top": 0.6, "left": -3.6, "attrs": {} },
{
"type": "wokwi-led",
"id": "led1",
"top": -51.6,
"left": 272.6,
"attrs": { "color": "magenta" }
},
{
"type": "wokwi-ky-040",
"id": "encoder1",
"top": -121.8,
"left": 346.9,
"rotate": 90,
"attrs": {}
},
{
"type": "wokwi-pushbutton",
"id": "btn1",
"top": -96.1,
"left": 435.5,
"rotate": 90,
"attrs": { "color": "green", "xray": "1" }
},
{
"type": "wokwi-led-bar-graph",
"id": "bargraph1",
"top": 33.6,
"left": 484.8,
"attrs": { "color": "lime" }
},
{
"type": "wokwi-led-bar-graph",
"id": "bargraph2",
"top": 139.2,
"left": 484.8,
"attrs": { "color": "lime" }
},
{
"type": "wokwi-led",
"id": "led2",
"top": -22.8,
"left": 426.2,
"attrs": { "color": "purple" }
},
{
"type": "wokwi-text",
"id": "text1",
"top": 28.8,
"left": 528,
"attrs": { "text": "RAMP 3A\n.\n.\nHIT" }
},
{
"type": "wokwi-text",
"id": "text2",
"top": 134.4,
"left": 528,
"attrs": { "text": "RAMP1\nHIT" }
},
{ "type": "wokwi-text", "id": "text3", "top": -48, "left": 307.2, "attrs": { "text": "TX" } },
{
"type": "wokwi-text",
"id": "text4",
"top": -19.2,
"left": 460.8,
"attrs": { "text": "SAVE" }
},
{
"type": "wokwi-text",
"id": "text5",
"top": -86.4,
"left": 499.2,
"attrs": { "text": "SUB" }
},
{
"type": "wokwi-text",
"id": "text6",
"top": 355.2,
"left": 364.8,
"attrs": { "text": "RAMP2" }
},
{ "type": "wokwi-text", "id": "text7", "top": 220.8, "left": 9.6, "attrs": { "text": "HIT" } },
{
"type": "wokwi-text",
"id": "text8",
"top": 355.2,
"left": 460.8,
"attrs": { "text": "RAMP1" }
},
{
"type": "wokwi-text",
"id": "text9",
"top": 355.2,
"left": 268.8,
"attrs": { "text": "RAMP3" }
},
{
"type": "wokwi-text",
"id": "text11",
"top": 316.8,
"left": -19.2,
"attrs": { "text": "TUNNEL" }
},
{
"type": "wokwi-neopixel-matrix",
"id": "ring1",
"top": -54.96,
"left": 54.08,
"attrs": { "pixleate": "1", "rows": "2", "cols": "6" }
},
{
"type": "wokwi-lcd1602",
"id": "lcd1",
"top": -195.2,
"left": 34.4,
"attrs": { "pins": "i2c" }
},
{
"type": "wokwi-slide-potentiometer",
"id": "pot1",
"top": 259.6,
"left": 229.2,
"rotate": 90,
"attrs": { "travelLength": "10" }
},
{
"type": "wokwi-slide-potentiometer",
"id": "pot2",
"top": 259.6,
"left": 325.2,
"rotate": 90,
"attrs": { "travelLength": "10" }
},
{
"type": "wokwi-slide-potentiometer",
"id": "pot3",
"top": 259.6,
"left": 421.2,
"rotate": 90,
"attrs": { "travelLength": "10" }
},
{
"type": "wokwi-slide-potentiometer",
"id": "pot4",
"top": 182.6,
"left": 22.6,
"rotate": 180,
"attrs": { "travelLength": "10" }
},
{
"type": "wokwi-slide-potentiometer",
"id": "pot5",
"top": 278.6,
"left": 22.6,
"rotate": 180,
"attrs": { "travelLength": "10" }
}
],
"connections": [
[ "mega:17", "led1:A", "green", [ "v0" ] ],
[ "mega:16", "led1:C", "green", [ "v0" ] ],
[ "mega:5V.2", "encoder1:VCC", "red", [ "v1", "h26.2" ] ],
[ "mega:GND.5", "encoder1:GND", "black", [ "v0.95", "h16.6" ] ],
[ "mega:19", "encoder1:CLK", "green", [ "v-19.2", "h97.9" ] ],
[ "mega:18", "encoder1:DT", "green", [ "v-28.8", "h107.9" ] ],
[ "mega:GND.5", "btn1:1.r", "black", [ "v0.95", "h26.2", "v19.2", "h86.4" ] ],
[ "mega:52", "btn1:2.r", "green", [ "v1.1", "h26.6" ] ],
[ "bargraph1:C10", "bargraph1:C9", "green", [ "h0" ] ],
[ "bargraph1:C9", "bargraph1:C8", "green", [ "h0" ] ],
[ "bargraph1:C8", "bargraph1:C7", "green", [ "h0" ] ],
[ "bargraph1:C7", "bargraph1:C6", "green", [ "h0" ] ],
[ "bargraph1:C6", "bargraph1:C5", "green", [ "h0" ] ],
[ "bargraph1:C5", "bargraph1:C4", "green", [ "h0" ] ],
[ "bargraph1:C4", "bargraph1:C3", "green", [ "h0" ] ],
[ "bargraph1:C3", "bargraph1:C2", "green", [ "h0" ] ],
[ "bargraph1:C2", "bargraph1:C1", "green", [ "h0" ] ],
[ "mega:34", "bargraph1:A1", "green", [ "v1.2", "h45.8", "v-19.2" ] ],
[ "mega:35", "bargraph1:A2", "green", [ "v1.2", "h35.8", "v-9.6" ] ],
[ "mega:37", "bargraph1:A4", "green", [ "v1.3", "h35.8" ] ],
[ "mega:36", "bargraph1:A3", "green", [ "v1.3", "h45.8", "v-9.6" ] ],
[ "mega:39", "bargraph1:A6", "green", [ "v1.15", "h35.8", "v9.6" ] ],
[ "mega:38", "bargraph1:A5", "green", [ "v1.15", "h45.8" ] ],
[ "mega:41", "bargraph1:A8", "green", [ "v1.25", "h35.8", "v19.2" ] ],
[ "mega:40", "bargraph1:A7", "green", [ "v1.25", "h45.8", "v19.2" ] ],
[ "mega:42", "bargraph1:A9", "green", [ "v1.1", "h45.8", "v19.2" ] ],
[ "bargraph2:C10", "bargraph2:C9", "green", [ "h0" ] ],
[ "bargraph2:C9", "bargraph2:C8", "green", [ "h0" ] ],
[ "bargraph2:C8", "bargraph2:C7", "green", [ "h0" ] ],
[ "bargraph2:C7", "bargraph2:C6", "green", [ "h0" ] ],
[ "bargraph2:C6", "bargraph2:C5", "green", [ "h0" ] ],
[ "bargraph2:C5", "bargraph2:C4", "green", [ "h0" ] ],
[ "bargraph2:C4", "bargraph2:C3", "green", [ "h0" ] ],
[ "bargraph2:C3", "bargraph2:C2", "green", [ "h0" ] ],
[ "bargraph2:C2", "bargraph2:C1", "green", [ "h0" ] ],
[
"mega:GND.5",
"bargraph2:C10",
"black",
[ "v0.95", "h26.2", "v19.2", "h91.6", "v36.5", "h32.98" ]
],
[ "mega:45", "bargraph2:A2", "green", [ "v1.2", "h35.8", "v9.6" ] ],
[ "mega:44", "bargraph2:A1", "green", [ "v1.2", "h45.8", "v9.6" ] ],
[ "mega:46", "bargraph2:A3", "green", [ "v1.05", "h45.8", "v19.2" ] ],
[ "mega:49", "bargraph2:A4", "green", [ "v1.15", "h35.8", "v19.2" ] ],
[ "mega:48", "bargraph2:A5", "green", [ "v1.15", "h45.8", "v28.8" ] ],
[ "mega:GND.5", "led2:C", "black", [ "v0.95", "h26.2", "v19.2", "h47.6" ] ],
[ "mega:33", "led2:A", "green", [ "v1.1", "h83.8" ] ],
[ "mega:GND.1", "ring1:GND", "black", [ "v0" ] ],
[ "ring1:VCC", "mega:5V", "red", [ "v163.2", "h36.1" ] ],
[ "mega:6", "ring1:DIN", "green", [ "v-9.6", "h-61.99" ] ],
[ "lcd1:VCC", "mega:5V", "red", [ "h-19.2", "v307.3", "h124.8" ] ],
[ "lcd1:SDA", "mega:20", "green", [ "h-38.4", "v192.2", "h326.4" ] ],
[ "lcd1:SCL", "mega:21", "green", [ "h-48", "v192.3", "h336" ] ],
[ "pot1:SIG", "mega:A2", "green", [ "v-9.6", "h-66.4" ] ],
[ "pot2:SIG", "mega:A3", "green", [ "v-19.2", "h-143.2" ] ],
[ "pot3:SIG", "mega:A4", "green", [ "v-28.8", "h-200.8" ] ],
[ "mega:5V", "pot1:VCC", "red", [ "v16.5", "h2.3" ] ],
[ "mega:5V", "pot2:VCC", "red", [ "v16.5", "h223.1" ] ],
[ "mega:5V", "pot3:VCC", "red", [ "v16.5", "h261.5" ] ],
[ "mega:GND.3", "pot1:GND", "black", [ "v198.9", "h127.05" ] ],
[ "mega:GND.3", "pot2:GND", "black", [ "v198.9", "h223.05" ] ],
[ "mega:GND.3", "pot3:GND", "black", [ "v198.9", "h319.05" ] ],
[ "mega:A0", "pot4:SIG", "green", [ "v0" ] ],
[ "mega:A5", "pot5:SIG", "green", [ "v0" ] ],
[ "mega:5V", "pot4:VCC", "red", [ "v0" ] ],
[ "mega:5V", "pot5:VCC", "red", [ "v0" ] ],
[ "mega:GND.3", "pot4:GND", "black", [ "v102.9", "h-151.35" ] ],
[ "mega:GND.3", "pot5:GND", "black", [ "v102.9", "h-160.95" ] ],
[ "mega:GND.3", "lcd1:GND", "black", [ "v-50.7", "h-170.55", "v-297.6" ] ],
[ "mega:24", "encoder1:SW", "green", [ "v0.95", "h55.4" ] ]
],
"dependencies": {}
}