#include <Wire.h>
#include <TFT_eSPI.h> // For graphics
#include <Adafruit_FT6206.h> // For touch
#include <FastLED.h>
unsigned long lastLedUpdate = 0;
const int ledMoveSpeed = 30; // Milliseconds between each LED update
int currentIteration = 0;
#define LED_PIN 3
#define STRIP_LEDS 512
#define RING_LEDS 35
#define NUM_LEDS (STRIP_LEDS + RING_LEDS)
#define RING_START STRIP_LEDS // 512
#define RING_END (STRIP_LEDS + RING_LEDS - 1)
bool lastNoteMode = false;
CRGB leds[NUM_LEDS];
TFT_eSPI tft = TFT_eSPI();
Adafruit_FT6206 ctp = Adafruit_FT6206();
//I learned that you can do multiple ints and const ints in one strand and found that much easier to manage
const int ButtonC = 22, ButtonD = 23, ButtonE = 24, ButtonF = 25, ButtonG = 26, ButtonA = 27, ButtonB = 28;
const int ButtonSharp = 29;
const int backButton = 30;
const int Speaker = 13;
bool noteMode = false;
byte colorarray[9][5] = {
{ 0, 255, 0 }, // green
{ 0, 255, 255 }, // cyan
{ 0, 0, 255 }, // blue
{ 255, 0, 255 }, // magenta
{ 255, 0, 0 }, // red
{ 255, 165, 0 }, // orange
{ 255, 255, 0 }, // yellow
{ 128, 255, 255 }, // white
{ 0, 0, 0 }, // black/off
};
byte noteArray[7][5] = {
{ 0, 250, 0 }, // green
{ 0, 0, 250 }, // blue
{ 0, 255, 255 }, // cyan
{ 255, 0, 255 }, // magenta
{ 255, 127, 0 }, // orange
{ 255, 0, 128 }, // pink
{ 255, 255, 0 }, // yellow
};
int C = 262, D = 294, E = 330, F = 349, G = 392, A = 440, B = 494;
int CS = 277, DS = 311, FS = 370, GS = 415, AS = 466;
int buttonState = 0;
int lastCycleLED = 0;
//bools are a bit confusing at first, but this just makes a true or false system for leds active
bool ledsActive = false;
bool ButtonPressed = false;
bool colorsLooped = false;
bool gameModesOpen = false;
bool mainMenuOpen = true;
bool freePlayOpen = false;
bool easyOpen = false;
bool mediumOpen = false;
bool hardOpen = false;
bool scoreOpen = false;
bool noteHit = false;
bool isTonePlaying = false;
int pressedNote = 0;
int pressedColorIdx = -1;
unsigned long lastMoveTime = 0;
int fallSpeed = 5;
int easyBoxY = 0;
int easyBoxColorIdx = 0;
bool canHit = false; // Is the note in the area
int score = 0;
// Tracks which note was played last
int lastNote = 0;
int currentFallSpeed = 7; // Default
enum LedMode {
MODE_IDLE,
MODE_NOTE
};
LedMode ledMode = MODE_IDLE;
void mainMenu() {
tft.fillScreen(0x1084);
tft.fillRect(75, 70, 170, 170, 0x318a);
tft.setTextColor(0xd69f);
tft.setTextSize(2);
tft.drawCentreString("Game Play", 160, 145, 2);
// Draw Bottom Button (Mode 2)
tft.fillRect(75, 270, 170, 170, 0x318a);
tft.drawCentreString("Free Play", 160, 345, 2);
tft.setTextSize(2);
tft.drawCentreString("Select game mode", 160, 20, 2);
gameModesOpen = false;
mainMenuOpen = true;
freePlayOpen = false;
easyOpen = false;
mediumOpen = false;
hardOpen = false;
}
void freePlay() {
tft.fillScreen(0x1084);
tft.setTextSize(3);
tft.drawCentreString("Press Buttons", 160, 110, 2);
tft.setTextSize(3);
tft.drawCentreString("To Play", 160, 300, 2);
tft.fillRect(75, 360, 170, 80, 0x318a); // y=360
tft.drawCentreString("back", 160, 392, 2);
gameModesOpen = false;
mainMenuOpen = false;
freePlayOpen = true;
easyOpen = false;
scoreOpen = false;
mediumOpen = false;
hardOpen = false;
}
void GameModes() {
tft.fillScreen(0x1084);
tft.setTextSize(2);
tft.setTextColor(0xd69f);
tft.drawCentreString("Select game mode", 160, 30, 2); // Title moved down to 30
// --- Box 1: Easy ---
tft.fillRect(75, 75, 170, 80, 0x318a); // y=75
tft.drawCentreString("Easy", 160, 107, 2);
// --- Box 2: Medium ---
tft.fillRect(75, 170, 170, 80, 0x318a); // y=170
tft.drawCentreString("Medium", 160, 202, 2);
// --- Box 3: Hard ---
tft.fillRect(75, 265, 170, 80, 0x318a); // y=265
tft.drawCentreString("Hard", 160, 297, 2);
// --- Box 4: back ---
tft.fillRect(75, 360, 170, 80, 0x318a); // y=360
tft.drawCentreString("back", 160, 392, 2);
gameModesOpen = true;
mainMenuOpen = false;
freePlayOpen = false;
easyOpen = false;
scoreOpen = false;
}
void easyMode() {
tft.fillScreen(0x1084);
tft.drawRect(100, 340, 120, 120, TFT_WHITE);
easyBoxY = 0; // Reset box to the top
currentFallSpeed = 10;
gameModesOpen = false;
mainMenuOpen = false;
freePlayOpen = false;
easyOpen = true;
scoreOpen = false;
mediumOpen = false;
hardOpen = false;
}
void mediumMode() {
tft.fillScreen(0x1084);
tft.drawRect(100, 340, 120, 120, TFT_WHITE);
easyBoxY = 0; // Reset box to the top
currentFallSpeed = 15;
gameModesOpen = false;
mainMenuOpen = false;
freePlayOpen = false;
easyOpen = false;
scoreOpen = false;
mediumOpen = true;
hardOpen = false;
}
void hardMode() {
tft.fillScreen(0x1084);
tft.drawRect(100, 340, 120, 120, TFT_WHITE);
easyBoxY = 0; // Reset box to the top
currentFallSpeed = 25;
gameModesOpen = false;
mainMenuOpen = false;
freePlayOpen = false;
easyOpen = false;
scoreOpen = false;
mediumOpen = false;
hardOpen = true;
}
void updateScoreDisplay() {
tft.fillRect(0, 0, 150, 30, 0x1084);
tft.setTextColor(TFT_WHITE);
tft.setCursor(10, 10);
tft.print("Score: ");
tft.print(score);
}
void gameExit() {
tft.fillScreen(0x1084);
tft.setTextColor(0xd69f);
tft.setTextSize(2);
tft.drawCentreString("your score is:", 160, 110, 2);
if (score < 0) {
tft.setTextColor(0xF800);
tft.setTextSize(4);
tft.drawCentreString(String(score), 160, 270, 2);
}
if (score == 0) {
tft.setTextColor(0xB5B6);
tft.setTextSize(4);
tft.drawCentreString(String(score), 160, 270, 2);
}
if (score > 0) {
tft.setTextColor(0x07E4);
tft.setTextSize(4);
tft.drawCentreString(String(score), 160, 270, 2);
}
tft.setTextSize(2);
tft.drawCentreString("press back button", 160, 350, 2);
tft.drawCentreString("to exit", 160, 400, 2);
gameModesOpen = false;
mainMenuOpen = false;
freePlayOpen = false;
easyOpen = false;
scoreOpen = true;
score = 0;
}
void updateRingScore() {
int lit = map(score, 0, 50, 0, RING_LEDS);
for (int i = 0; i < RING_LEDS; i++) {
if (i < lit) leds[RING_START + i] = CRGB::Green;
else leds[RING_START + i] = CRGB::Black;
}
}
void setup() {
Serial.begin(115200);
while (!Serial);
// ADDED
Serial.print("Size of \"leds\" = ");
Serial.print(sizeof(leds));
Serial.println(" bytes");
// 1. Start Display
tft.init();
tft.setRotation(0); // Portrait (320x480)
tft.fillScreen(0x1084);
tft.drawCentreString("Testing Touch...", 160, 240, 2);
// 2. Start I2C and Touch
Wire.begin();
if (!ctp.begin(40)) {
Serial.println("FAILED to find FT6206 touch controller!");
tft.fillScreen(TFT_RED);
tft.drawCentreString("TOUCH CHIP NOT FOUND", 160, 200, 4);
} else {
Serial.println("Touch controller found!");
tft.fillScreen(TFT_BLACK);
mainMenu();
}
//these first lines set up the led strip
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(3);
FastLED.clear();
//these lines set up all the buttons and the speaker
pinMode(ButtonC, INPUT_PULLUP);
pinMode(ButtonD, INPUT_PULLUP);
pinMode(ButtonE, INPUT_PULLUP);
pinMode(ButtonF, INPUT_PULLUP);
pinMode(ButtonG, INPUT_PULLUP);
pinMode(ButtonA, INPUT_PULLUP);
pinMode(ButtonB, INPUT_PULLUP);
pinMode(ButtonSharp, INPUT_PULLUP);
pinMode(backButton, INPUT_PULLUP);
pinMode(Speaker, OUTPUT);
}
void loop() {
// 1. GET THE CURRENT TIME (This fixes your error!)
unsigned long currentMillis = millis();
ledMode = noteMode ? MODE_NOTE : MODE_IDLE;
bool currentNoteMode =
digitalRead(ButtonC) == LOW ||
digitalRead(ButtonD) == LOW ||
digitalRead(ButtonE) == LOW ||
digitalRead(ButtonF) == LOW ||
digitalRead(ButtonG) == LOW ||
digitalRead(ButtonA) == LOW ||
digitalRead(ButtonB) == LOW;
if (currentNoteMode && !lastNoteMode) {
// just set a flag — DO NOT draw yet
ledsActive = true;
}
noteMode = currentNoteMode;
lastNoteMode = currentNoteMode;
// --- 1. BACK BUTTON LOGIC ---
if (digitalRead(backButton) == LOW) {
if (gameModesOpen || freePlayOpen || scoreOpen) mainMenu();
else if (easyOpen || mediumOpen || hardOpen) {
gameExit();
}
delay(200);
}
noteMode =
digitalRead(ButtonC) == LOW ||
digitalRead(ButtonD) == LOW ||
digitalRead(ButtonE) == LOW ||
digitalRead(ButtonF) == LOW ||
digitalRead(ButtonG) == LOW ||
digitalRead(ButtonA) == LOW ||
digitalRead(ButtonB) == LOW;
bool frameUpdated = renderLEDs(); // THEN
if (frameUpdated) FastLED.show();
// --- 2. TOUCH LOGIC (Menu Navigation) ---
if (ctp.touched()) {
TS_Point p = ctp.getPoint();
int x = map(p.x, 0, 240, 0, 240);
int y = map(p.y, 0, 320, 0, 320);
if (mainMenuOpen) {
if (x > 75 && x < 245 && y > 70 && y < 240) {
GameModes();
delay(300);
}
else if (x > 75 && x < 245 && y > 270 && y < 440) {
freePlay();
delay(300);
}
} else if (gameModesOpen) {
if (y > 75 && y < 155) {
easyMode();
delay(300);
}
else if (y > 170 && y < 250) {
mediumMode();
delay(300);
}
else if (y > 265 && y < 345) {
hardMode();
delay(300);
}
else if (y > 360) {
mainMenu();
delay(300);
}
} else if (freePlayOpen || easyOpen || mediumOpen || hardOpen) {
if (y > 360) {
noTone(Speaker);
mainMenu();
delay(300);
}
}
}
// --- 3. ANIMATION LOGIC ---
if ((easyOpen || mediumOpen || hardOpen) && !scoreOpen) {
if (currentMillis - lastMoveTime >= 10) {
lastMoveTime = currentMillis;
// Erase trail - uses currentFallSpeed for a clean wipe
tft.fillRect(102, easyBoxY - (currentFallSpeed + 5), 116, (currentFallSpeed + 5), 0x1084);
easyBoxY += currentFallSpeed;
// Hit-box drawing (ONLY happens here inside the active game check)
tft.drawRect(100, 340, 120, 120, TFT_WHITE);
// Respawn logic
if (easyBoxY > 480) {
tft.fillRect(102, 0, 116, 480, 0x1084);
easyBoxY = 50;
easyBoxColorIdx = random(0, 7);
noteHit = false;
}
// Hit zone range check
canHit = (easyBoxY >= 330 && easyBoxY <= 450);
if (!noteHit) {
uint16_t noteColor = tft.color565(noteArray[easyBoxColorIdx][0],
noteArray[easyBoxColorIdx][1],
noteArray[easyBoxColorIdx][2]);
tft.fillRect(102, easyBoxY, 116, 116, noteColor);
}
}
updateScoreDisplay(); // Keep score visible during gameplay
}
// --- 4. READ INPUTS & HIT DETECTION ---
if ((easyOpen || mediumOpen || hardOpen || freePlayOpen) && !scoreOpen) {
// Detect which button is pressed
if (digitalRead(ButtonC) == LOW) {
pressedNote = (digitalRead(ButtonSharp) == LOW) ? CS : C;
pressedColorIdx = 0;
}
else if (digitalRead(ButtonD) == LOW) {
pressedNote = (digitalRead(ButtonSharp) == LOW) ? DS : D;
pressedColorIdx = 1;
}
else if (digitalRead(ButtonE) == LOW) {
pressedNote = E;
pressedColorIdx = 2;
}
else if (digitalRead(ButtonF) == LOW) {
pressedNote = (digitalRead(ButtonSharp) == LOW) ? FS : F;
pressedColorIdx = 3;
}
else if (digitalRead(ButtonG) == LOW) {
pressedNote = (digitalRead(ButtonSharp) == LOW) ? GS : G;
pressedColorIdx = 4;
}
else if (digitalRead(ButtonA) == LOW) {
pressedNote = (digitalRead(ButtonSharp) == LOW) ? AS : A;
pressedColorIdx = 5;
}
else if (digitalRead(ButtonB) == LOW) {
pressedNote = B;
pressedColorIdx = 6;
}
if (pressedNote != 0) {
// Game Hit Logic (Timed modes only)
if ((easyOpen || mediumOpen || hardOpen) && canHit && !noteHit) {
noteHit = true;
if (pressedColorIdx == easyBoxColorIdx) {
score++;
tft.drawRect(100, 340, 120, 120, TFT_GREEN);
} else {
score--;
tft.drawRect(100, 340, 120, 120, TFT_RED);
}
updateScoreDisplay();
tft.fillRect(102, easyBoxY, 116, 116, 0x1084);
}
// Speaker Gate: Only play sound if it wasn't already playing
if (!isTonePlaying) {
tone(Speaker, pressedNote);
isTonePlaying = true;
}
} else {
// Reset sound when button is released
if (isTonePlaying) {
noTone(Speaker);
isTonePlaying = false;
}
if ((easyOpen || mediumOpen || hardOpen) && !noteHit) {
tft.drawRect(100, 340, 120, 120, TFT_WHITE);
}
}
} else {
// Safety stop for sound
if (isTonePlaying) {
noTone(Speaker);
isTonePlaying = false;
}
}
// --- 3. TARGET NOTE MAPPING (CRITICAL FIX) ---
int targetNote = 0;
if (easyBoxColorIdx == 0) targetNote = C; // Green
else if (easyBoxColorIdx == 1) targetNote = E; // Cyan
else if (easyBoxColorIdx == 2) targetNote = D; // Blue
else if (easyBoxColorIdx == 3) targetNote = F; // Magenta
else if (easyBoxColorIdx == 4) targetNote = G; // Red
else if (easyBoxColorIdx == 5) targetNote = A; // Orange
else if (easyBoxColorIdx == 6) targetNote = B; // Yellow
// --- 4. MATCHING & CLEAN ERASE ---
if (pressedNote > 0 && canHit && !noteHit) {
if (pressedNote == targetNote) {
score++;
noteHit = true; // Mark as hit so no miss penalty occurs
tft.fillRect(101, easyBoxY - 5, 118, 126, 0x1084);
easyBoxY = 481;
tft.drawRect(100, 340, 120, 120, TFT_GREEN);
} else {
// Optional: immediate penalty for pressing the WRONG note
score--;
tft.drawRect(100, 340, 120, 120, TFT_RED);
}
updateScoreDisplay();
}
if (freePlayOpen || easyOpen || mediumOpen || hardOpen) {
int note = 0;
CRGB noteColor = CRGB::Black; // this is just a temporary color that will change with the notes
//these lines check if a button is pressed, and sets a specific note and color for it.
//these lines also add a sharp note for those that have them
//I found it much easier to set the lighter tones as the chsv because when i tried it with CRGB it always turned out white
if (digitalRead(ButtonC) == LOW) {
note = (digitalRead(ButtonSharp) == LOW) ? CS : C;
noteColor = (digitalRead(ButtonSharp) == LOW) ? CHSV(96, 150, 255) : CRGB(noteArray[0][0], noteArray[0][1], noteArray[0][2]); // Green
} else if (digitalRead(ButtonD) == LOW) {
note = (digitalRead(ButtonSharp) == LOW) ? DS : D;
noteColor = (digitalRead(ButtonSharp) == LOW) ? CHSV(160, 150, 255) : CRGB(noteArray[1][0], noteArray[1][1], noteArray[1][2]); // Blue
} else if (digitalRead(ButtonE) == LOW) {
note = E;
noteColor = CRGB(noteArray[2][0], noteArray[2][1], noteArray[2][2]); // Cyan
} else if (digitalRead(ButtonF) == LOW) {
note = (digitalRead(ButtonSharp) == LOW) ? FS : F;
noteColor = (digitalRead(ButtonSharp) == LOW) ? CHSV(192, 140, 255) : CRGB(noteArray[3][0], noteArray[3][1], noteArray[3][2]); // Magenta
} else if (digitalRead(ButtonG) == LOW) {
note = (digitalRead(ButtonSharp) == LOW) ? GS : G;
noteColor = (digitalRead(ButtonSharp) == LOW) ? CHSV(32, 150, 255) : CRGB(noteArray[4][0], noteArray[4][1], noteArray[4][2]); // Orange
} else if (digitalRead(ButtonA) == LOW) {
note = (digitalRead(ButtonSharp) == LOW) ? AS : A;
noteColor = (digitalRead(ButtonSharp) == LOW) ? CHSV(240, 110, 255) : CRGB(noteArray[5][0], noteArray[5][1], noteArray[5][2]); // Pink
} else if (digitalRead(ButtonB) == LOW) {
note = B;
noteColor = CRGB(noteArray[6][0], noteArray[6][1], noteArray[6][2]); // Yellow
}
//all these if and else statements make the leds and notes play and glow
if (note > 0) { //checks if the note int is a specific number
tone(Speaker, note); // this plays what the note is equal to, which changes with the button
if (!ledsActive || note != lastNote) { // this check if the led is NOT active
//these lines make the led strip glow a specific color
ledsActive = true;
lastNote = note; // Save the current note
}
ButtonPressed = true;
}
else {
noTone(Speaker);
if (ledsActive) {
// FastLED.clear();
ledsActive = false;
lastNote = 0;
}
}
}
//this just turns off the leds if no button is pressed
noteMode = (
digitalRead(ButtonC) == LOW ||
digitalRead(ButtonD) == LOW ||
digitalRead(ButtonE) == LOW ||
digitalRead(ButtonF) == LOW ||
digitalRead(ButtonG) == LOW ||
digitalRead(ButtonA) == LOW ||
digitalRead(ButtonB) == LOW
);
if (!noteMode && lastNoteMode) {
colorsLooped = false;
currentIteration = 0;
}
}
bool renderLEDs() {
static int lastNote = 0;
static bool hasNote = false;
int note = 0;
if (digitalRead(ButtonC) == LOW) note = C;
else if (digitalRead(ButtonD) == LOW) note = D;
else if (digitalRead(ButtonE) == LOW) note = E;
else if (digitalRead(ButtonF) == LOW) note = F;
else if (digitalRead(ButtonG) == LOW) note = G;
else if (digitalRead(ButtonA) == LOW) note = A;
else if (digitalRead(ButtonB) == LOW) note = B;
if (note == 0) {
if (hasNote) {
fill_solid(leds, NUM_LEDS, CRGB::Black);
hasNote = false;
return true;
}
return false;
}
if (note != lastNote || !hasNote) {
lastNote = note;
hasNote = true;
CRGB color = CRGB::Blue; // replace later with mapping
fill_solid(leds, NUM_LEDS, color);
return true;
}
return false;
}Arduino Mega: 50(CIPO), 51(COPI), 52(SCK), 53(CS)