#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SPI.h>
#include <MFRC522.h>
#include <math.h>
// =============================================================================
// DISPLAY & HARDWARE
// =============================================================================
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_ADDR 0x3C
#define JOY_X_PIN 34
#define JOY_Y_PIN 35
#define JOY_SW_PIN 32
#define BUZZER_PIN 27
#define RFID_SS_PIN 4
#define RFID_RST_PIN 5
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
MFRC522 mfrc522(RFID_SS_PIN, RFID_RST_PIN);
// =============================================================================
// AUTHORIZED RFID CARDS
// =============================================================================
const String ALLOWED_UID_1 = "01020304";
const String ALLOWED_UID_2 = "F15F7F01";
// =============================================================================
// BASIC SOUND EFFECTS
// =============================================================================
void playSfx(unsigned int frequency, unsigned long durationMs) {
tone(BUZZER_PIN, frequency, durationMs);
}
void playGameOverSfx() {
playSfx(220, 180);
}
void playWinSfx() {
playSfx(1568, 90);
}
// =============================================================================
// SYSTEM STATES
// =============================================================================
enum SystemState {
STATE_SIGN_IN,
STATE_MENU,
STATE_SNAKE,
STATE_BREAKOUT,
STATE_INVADERS,
STATE_GLIDER,
STATE_MUSIC
};
SystemState currentState = STATE_SIGN_IN;
String currentPlayerUID = "";
bool isHardMode = false;
int menuSelection = 0;
int endScreenSelection = 0;
// =============================================================================
// JOYSTICK
// =============================================================================
int restX = 2048;
int restY = 2048;
int readStableJoystickAxis(uint8_t pin) {
int samples[5];
for (int i = 0; i < 5; i++) {
samples[i] = analogRead(pin);
delayMicroseconds(250);
}
for (int i = 1; i < 5; i++) {
int value = samples[i];
int j = i - 1;
while (j >= 0 && samples[j] > value) {
samples[j + 1] = samples[j];
j--;
}
samples[j + 1] = value;
}
return samples[2];
}
int readJoystickX() {
return 4095 - readStableJoystickAxis(JOY_Y_PIN);
}
int readJoystickY() {
return 4095 - readStableJoystickAxis(JOY_X_PIN);
}
// =============================================================================
// BUTTON
// =============================================================================
bool lastButtonState = HIGH;
unsigned long buttonPressStartTime = 0;
bool buttonBeingHeld = false;
unsigned long lastButtonDebounceTime = 0;
#define BUTTON_DEBOUNCE_DELAY 50
#define CHEAT_HOLD_TIME 3000
#define CHEAT_GRACE_PERIOD 1000
unsigned long lastMenuMove = 0;
// =============================================================================
// CHEAT FLAGS
// =============================================================================
bool snakeCheatActive = false;
bool breakoutCheatActive = false;
bool invadersCheatActive = false;
bool gliderCheatActive = false;
// =============================================================================
// SNAKE
// =============================================================================
#define SNAKE_SIZE 4
#define FOOD_SIZE 4
#define MAX_SNAKE_LENGTH 64
#define SNAKE_MOVE_SPEED 150
#define TURN_DEBOUNCE_MS 180
#define SNAKE_WIN_SCORE 40
#define SNAKE_TOP_OFFSET 12
#define DIR_UP 0
#define DIR_RIGHT 1
#define DIR_DOWN 2
#define DIR_LEFT 3
int snake_x[MAX_SNAKE_LENGTH];
int snake_y[MAX_SNAKE_LENGTH];
int snake_length = 3;
int food_x = 0;
int food_y = 0;
int current_dir = DIR_RIGHT;
int next_dir = DIR_RIGHT;
int queued_dir = -1;
bool dirChangedThisTick = false;
bool snakeStart = true;
bool snakeGameOver = false;
bool snakeWon = false;
bool snakePaused = false;
int snakeScore = 0;
bool joyMovedSnake = false;
unsigned long lastTurnTimeSnake = 0;
int lastFlickAxisSnake = 0;
unsigned long lastSnakeMoveTime = 0;
// =============================================================================
// BREAKOUT
// =============================================================================
#define PADDLE_WIDTH 24
#define PADDLE_HEIGHT 4
#define PADDLE_Y (OLED_HEIGHT - 6)
#define MIN_PADDLE_SPEED 1.0
#define MAX_PADDLE_SPEED 6.0
#define BALL_SIZE 3
#define INITIAL_BRICK_ROWS 3
#define MAX_BRICK_ROWS 7
#define BRICK_COLS 8
#define BRICK_WIDTH 14
#define BRICK_HEIGHT 3
#define BRICK_GAP 1
#define BRICK_OFFSET_TOP 8
#define BRICK_OFFSET_LEFT 1
float paddleX = (OLED_WIDTH - PADDLE_WIDTH) / 2.0;
float ballX;
float ballY;
float ballDX;
float ballDY;
bool bricks[MAX_BRICK_ROWS][BRICK_COLS];
int currentBrickRows = INITIAL_BRICK_ROWS;
int remainingBricks = 0;
int breakoutScore = 0;
int breakoutLives = 3;
bool breakoutStart = true;
bool breakoutPaused = false;
bool breakoutGameOver = false;
bool breakoutWon = false;
unsigned long lastBreakoutFrameTime = 0;
const int BREAKOUT_FRAME_DELAY = 25;
// =============================================================================
// SPACE INVADERS
// =============================================================================
#define SHIP_WIDTH 9
#define SHIP_HEIGHT 5
#define ALIEN_COLS 9
#define ALIEN_ROWS 3
#define ALIEN_WIDTH 8
#define ALIEN_HEIGHT 6
#define ALIEN_GAP_X 4
#define ALIEN_GAP_Y 3
#define MAX_BULLETS 12
#define MAX_ENEMY_BULLETS 6
struct Bullet {
float x;
float y;
bool active;
};
struct Alien {
float x;
float y;
bool alive;
};
float shipX;
float shipY;
Alien invaders[ALIEN_ROWS][ALIEN_COLS];
Bullet bullets[MAX_BULLETS];
Bullet enemyBullets[MAX_ENEMY_BULLETS];
int invadersScore = 0;
int maxPlayerHealth = 60;
int invadersHealth = 60;
bool invadersStart = true;
bool invadersPaused = false;
bool invadersGameOver = false;
bool invadersWon = false;
int alienDirection = 1;
unsigned long lastInvaderMove = 0;
unsigned long lastInvaderFrame = 0;
unsigned long lastEnemyShootTime = 0;
unsigned long lastInvadersMachineGunShot = 0;
const unsigned long INVADERS_MACHINE_GUN_INTERVAL = 100;
const int INVADER_FRAME_DELAY = 0;
bool isMothershipStage = false;
float mothershipX = 0;
float mothershipY = 16;
int mothershipHealth = 150;
int mothershipMaxHealth = 150;
int mothershipDir = 1;
unsigned long lastMothershipShootBurst = 0;
int barrageBulletsLeft = 0;
unsigned long lastBarrageBulletTime = 0;
// =============================================================================
// GLIDER
// =============================================================================
#define MAX_CHOPS 2
#define MAX_TROOPERS 25
#define MAX_GLIDER_SHELLS 128
#define GLIDER_AUTO_FIRE_DELAY 150
struct Helicopter {
float x;
float y;
int dir;
bool active;
unsigned long nextDropTime;
bool isHuge;
};
struct Trooper {
float x;
float y;
bool active;
bool hasChute;
bool chuteDestroyed;
bool landed;
int side;
};
struct GliderShell {
float x;
float y;
float dx;
float dy;
bool active;
};
Helicopter choppers[MAX_CHOPS];
Trooper troopers[MAX_TROOPERS];
GliderShell gliderShells[MAX_GLIDER_SHELLS];
float turretX = 64.0;
float turretAngle = 1.5708;
int leftTrooperCount = 0;
int rightTrooperCount = 0;
int gliderScore = 0;
int gliderLives = 3;
bool gliderStart = true;
bool gliderPaused = false;
bool gliderGameOver = false;
bool gliderWon = false;
unsigned long lastGliderFrame = 0;
unsigned long lastChopperSpawn = 0;
unsigned long lastGliderAutoFire = 0;
unsigned long lastRFIDRecovery = 0;
// =============================================================================
// MUSIC SYSTEM
// =============================================================================
//
// These are original passive-buzzer melodies. They are intentionally written
// as adult/radio-style instrumental melodies rather than nursery-style tunes.
//
// Each song is monophonic because the passive buzzer can only play one note
// at a time.
//
// =============================================================================
#define REST 0
// Note frequencies
#define C3 131
#define CS3 139
#define D3 147
#define DS3 156
#define E3 165
#define F3 175
#define FS3 185
#define G3 196
#define GS3 208
#define A3 220
#define AS3 233
#define B3 247
#define C4 262
#define CS4 277
#define D4 294
#define DS4 311
#define E4 330
#define F4 349
#define FS4 370
#define G4 392
#define GS4 415
#define A4 440
#define AS4 466
#define B4 494
#define C5 523
#define CS5 554
#define D5 587
#define DS5 622
#define E5 659
#define F5 698
#define FS5 740
#define G5 784
#define GS5 831
#define A5 880
#define AS5 932
#define B5 988
#define C6 1047
#define D6 1175
#define E6 1319
#define F6 1397
#define G6 1568
#define A6 1760
// -----------------------------------------------------------------------------
// A - After Midnight
// -----------------------------------------------------------------------------
const uint16_t songA[] = {
A3, C4, E4, G4,
E4, C4, A3, REST,
A3, D4, F4, A4,
G4, F4, D4, REST,
A3, C4, E4, A4,
G4, E4, C4, B3
};
const uint16_t durA[] = {
180,180,180,260,
180,180,220,120,
180,180,180,260,
180,180,220,120,
180,180,180,260,
180,180,180,320
};
// -----------------------------------------------------------------------------
// B - Backseat Radio
// -----------------------------------------------------------------------------
const uint16_t songB[] = {
E4,G4,A4,B4,
A4,G4,E4,D4,
E4,G4,B4,D5,
B4,A4,G4,E4,
E4,G4,A4,C5,
B4,G4,E4,D4
};
const uint16_t durB[] = {
150,150,180,260,
150,150,180,220,
150,150,180,280,
150,150,180,220,
150,150,180,260,
150,150,180,320
};
// -----------------------------------------------------------------------------
// C - City Lights
// -----------------------------------------------------------------------------
const uint16_t songC[] = {
C4,E4,G4,A4,
G4,E4,C4,D4,
E4,G4,B4,A4,
G4,E4,D4,C4,
C4,E4,G4,C5,
B4,G4,E4,D4
};
const uint16_t durC[] = {
140,140,160,260,
140,140,160,200,
140,140,160,260,
140,140,160,240,
140,140,160,280,
140,140,160,330
};
// -----------------------------------------------------------------------------
// D - Downtown
// -----------------------------------------------------------------------------
const uint16_t songD[] = {
D4,F4,A4,C5,
A4,F4,D4,C4,
D4,G4,B4,D5,
B4,G4,F4,D4,
A3,C4,E4,A4,
G4,E4,C4,A3
};
const uint16_t durD[] = {
160,160,180,280,
160,160,180,220,
160,160,180,280,
160,160,180,220,
160,160,180,280,
160,160,180,340
};
// -----------------------------------------------------------------------------
// E - Electric Avenue
// -----------------------------------------------------------------------------
const uint16_t songE[] = {
E4,E4,G4,A4,
B4,A4,G4,E4,
E4,G4,A4,C5,
B4,A4,G4,E4,
D4,E4,G4,B4,
A4,G4,E4,D4
};
const uint16_t durE[] = {
130,130,160,200,
180,150,150,220,
130,160,160,240,
180,150,150,220,
130,160,160,240,
180,150,150,320
};
// -----------------------------------------------------------------------------
// F - Fade Into Neon
// -----------------------------------------------------------------------------
const uint16_t songF[] = {
F4,A4,C5,E5,
C5,A4,F4,E4,
F4,G4,A4,C5,
A4,G4,F4,E4,
D4,F4,A4,D5,
C5,A4,F4,E4
};
const uint16_t durF[] = {
170,170,190,280,
170,170,190,220,
170,170,190,280,
170,170,190,220,
170,170,190,280,
170,170,190,340
};
// -----------------------------------------------------------------------------
// G - Golden Hour
// -----------------------------------------------------------------------------
const uint16_t songG[] = {
G3,B3,D4,G4,
F4,D4,B3,A3,
G3,C4,E4,G4,
A4,G4,E4,C4,
G3,B3,D4,A4,
G4,D4,B3,G3
};
const uint16_t durG[] = {
180,180,200,300,
180,180,200,220,
180,180,200,300,
180,180,200,220,
180,180,200,300,
180,180,200,360
};
// -----------------------------------------------------------------------------
// H - Highway Static
// -----------------------------------------------------------------------------
const uint16_t songH[] = {
E4,F4,G4,B4,
A4,G4,F4,E4,
E4,G4,A4,C5,
B4,A4,G4,E4,
F4,A4,C5,D5,
C5,A4,G4,E4
};
const uint16_t durH[] = {
120,120,160,260,
160,140,140,200,
120,160,160,260,
160,140,140,220,
150,160,180,280,
160,160,180,340
};
// -----------------------------------------------------------------------------
// I - In The Dark
// -----------------------------------------------------------------------------
const uint16_t songI[] = {
A3,C4,E4,G4,
A4,G4,E4,C4,
A3,D4,F4,A4,
G4,F4,D4,A3,
E4,G4,B4,D5,
C5,B4,G4,E4
};
const uint16_t durI[] = {
200,160,160,250,
180,160,160,220,
200,160,160,250,
180,160,160,220,
200,160,160,280,
180,160,160,360
};
// -----------------------------------------------------------------------------
// J - Jet Black
// -----------------------------------------------------------------------------
const uint16_t songJ[] = {
D4,F4,A4,D5,
C5,A4,F4,D4,
E4,G4,B4,E5,
D5,B4,G4,E4,
F4,A4,C5,F5,
E5,C5,A4,F4
};
const uint16_t durJ[] = {
140,140,160,250,
160,140,140,210,
140,140,160,250,
160,140,140,210,
140,140,160,250,
160,140,140,340
};
// -----------------------------------------------------------------------------
// K - Midnight Drive
// -----------------------------------------------------------------------------
const uint16_t songK[] = {
B3,D4,F4,A4,
G4,F4,D4,B3,
C4,E4,G4,B4,
A4,G4,E4,C4,
D4,F4,A4,C5,
B4,A4,F4,D4
};
const uint16_t durK[] = {
160,160,170,260,
160,160,170,220,
160,160,170,260,
160,160,170,220,
160,160,170,260,
160,160,170,350
};
// -----------------------------------------------------------------------------
// L - Late Night
// -----------------------------------------------------------------------------
const uint16_t songL[] = {
C4,D4,E4,G4,
A4,G4,E4,D4,
C4,E4,G4,A4,
G4,E4,D4,C4,
D4,F4,A4,C5,
A4,F4,D4,C4
};
const uint16_t durL[] = {
180,140,180,230,
200,150,180,200,
180,140,180,250,
200,150,180,220,
180,140,180,250,
200,150,180,350
};
// -----------------------------------------------------------------------------
// M - Midnight City
// -----------------------------------------------------------------------------
const uint16_t songM[] = {
E4,G4,B4,E5,
D5,B4,G4,E4,
F4,A4,C5,F5,
E5,C5,A4,F4,
G4,B4,D5,G5,
E5,D5,B4,G4
};
const uint16_t durM[] = {
130,130,160,260,
150,130,160,210,
130,130,160,260,
150,130,160,210,
130,130,160,270,
150,130,160,350
};
// -----------------------------------------------------------------------------
// N - Nightshift
// -----------------------------------------------------------------------------
const uint16_t songN[] = {
D4,E4,F4,A4,
G4,F4,E4,D4,
D4,F4,A4,C5,
B4,A4,F4,D4,
E4,G4,B4,D5,
C5,B4,G4,E4
};
const uint16_t durN[] = {
150,150,180,250,
160,150,150,210,
150,150,180,250,
160,150,150,210,
150,150,180,270,
160,150,150,350
};
// -----------------------------------------------------------------------------
// O - Open Road
// -----------------------------------------------------------------------------
const uint16_t songO[] = {
G3,A3,B3,D4,
E4,D4,B3,A3,
G3,C4,E4,G4,
A4,G4,E4,C4,
D4,E4,G4,B4,
A4,G4,E4,D4
};
const uint16_t durO[] = {
180,140,160,240,
180,150,160,220,
180,140,160,250,
180,150,160,220,
180,140,160,250,
180,150,160,350
};
// -----------------------------------------------------------------------------
// P - Pulse
// -----------------------------------------------------------------------------
const uint16_t songP[] = {
E4,E4,G4,B4,
A4,G4,E4,D4,
E4,G4,A4,C5,
B4,A4,G4,E4,
D4,F4,A4,C5,
B4,A4,F4,D4
};
const uint16_t durP[] = {
110,110,140,230,
140,120,140,200,
110,140,140,230,
140,120,140,200,
110,140,140,230,
140,120,140,330
};
// -----------------------------------------------------------------------------
// Q - Quiet Fire
// -----------------------------------------------------------------------------
const uint16_t songQ[] = {
A3,C4,D4,F4,
E4,D4,C4,A3,
A3,D4,F4,A4,
G4,F4,D4,A3,
C4,E4,G4,B4,
A4,G4,E4,C4
};
const uint16_t durQ[] = {
200,160,160,250,
180,160,160,220,
200,160,160,250,
180,160,160,220,
200,160,160,280,
180,160,160,360
};
// -----------------------------------------------------------------------------
// R - Redline
// -----------------------------------------------------------------------------
const uint16_t songR[] = {
D4,D4,F4,A4,
C5,A4,F4,D4,
E4,E4,G4,B4,
D5,B4,G4,E4,
F4,A4,C5,D5,
C5,A4,F4,D4
};
const uint16_t durR[] = {
110,110,140,230,
150,130,140,200,
110,110,140,230,
150,130,140,200,
130,140,160,250,
150,130,140,330
};
// -----------------------------------------------------------------------------
// S - Streetlights
// -----------------------------------------------------------------------------
const uint16_t songS[] = {
C4,E4,G4,B4,
A4,G4,E4,C4,
D4,F4,A4,C5,
B4,A4,F4,D4,
E4,G4,B4,D5,
C5,B4,G4,E4
};
const uint16_t durS[] = {
160,160,180,250,
170,150,170,210,
160,160,180,250,
170,150,170,210,
160,160,180,270,
170,150,170,350
};
// -----------------------------------------------------------------------------
// T - Transmission
// -----------------------------------------------------------------------------
const uint16_t songT[] = {
F4,G4,A4,C5,
B4,A4,G4,F4,
E4,G4,B4,D5,
C5,B4,G4,E4,
F4,A4,C5,E5,
D5,C5,A4,F4
};
const uint16_t durT[] = {
130,130,160,250,
160,140,140,210,
130,130,160,250,
160,140,140,210,
130,130,160,270,
160,140,140,350
};
// -----------------------------------------------------------------------------
// U - Underground
// -----------------------------------------------------------------------------
const uint16_t songU[] = {
E3,G3,B3,D4,
C4,B3,G3,E3,
F3,A3,C4,E4,
D4,C4,A3,F3,
G3,B3,D4,G4,
F4,D4,B3,G3
};
const uint16_t durU[] = {
180,160,180,250,
180,160,180,220,
180,160,180,250,
180,160,180,220,
180,160,180,270,
180,160,180,350
};
// -----------------------------------------------------------------------------
// V - Velvet Sky
// -----------------------------------------------------------------------------
const uint16_t songV[] = {
A3,C4,E4,A4,
G4,E4,C4,A3,
D4,F4,A4,D5,
C5,A4,F4,D4,
E4,G4,B4,E5,
D5,B4,G4,E4
};
const uint16_t durV[] = {
200,160,160,270,
180,160,160,220,
200,160,160,270,
180,160,160,220,
200,160,160,280,
180,160,160,360
};
// -----------------------------------------------------------------------------
// W - Wild Side
// -----------------------------------------------------------------------------
const uint16_t songW[] = {
G4,A4,B4,D5,
C5,B4,A4,G4,
F4,A4,C5,E5,
D5,C5,A4,F4,
G4,B4,D5,F5,
E5,D5,B4,G4
};
const uint16_t durW[] = {
120,120,150,240,
150,130,130,200,
120,120,150,250,
150,130,130,210,
120,120,150,260,
150,130,130,350
};
// -----------------------------------------------------------------------------
// X - X-Ray
// -----------------------------------------------------------------------------
const uint16_t songX[] = {
E4,FS4,G4,B4,
A4,G4,FS4,E4,
D4,FS4,A4,C5,
B4,A4,FS4,D4,
E4,G4,B4,D5,
C5,B4,G4,E4
};
const uint16_t durX[] = {
130,130,150,240,
150,130,130,210,
130,130,150,240,
150,130,130,210,
130,130,150,250,
150,130,130,350
};
// -----------------------------------------------------------------------------
// Y - Young Again
// -----------------------------------------------------------------------------
const uint16_t songY[] = {
C4,D4,E4,G4,
A4,G4,E4,D4,
C4,E4,G4,B4,
A4,G4,E4,C4,
D4,F4,A4,C5,
B4,A4,F4,D4
};
const uint16_t durY[] = {
170,140,170,240,
180,150,170,210,
170,140,170,250,
180,150,170,220,
170,140,170,250,
180,150,170,350
};
// -----------------------------------------------------------------------------
// Z - Zero Hour
// -----------------------------------------------------------------------------
const uint16_t songZ[] = {
B3,D4,FS4,A4,
G4,FS4,D4,B3,
C4,E4,G4,B4,
A4,G4,E4,C4,
D4,FS4,A4,C5,
B4,A4,FS4,D4
};
const uint16_t durZ[] = {
140,140,170,250,
160,140,160,210,
140,140,170,250,
160,140,160,210,
140,140,170,260,
160,140,160,350
};
// =============================================================================
// MUSIC SONG STRUCTURE
// =============================================================================
struct MusicSong {
const char* title;
const uint16_t* notes;
const uint16_t* durations;
uint16_t length;
};
#define SONG_LEN(x) (sizeof(x) / sizeof(x[0]))
MusicSong musicLibrary[] = {
{"A AFTER MIDNIGHT", songA, durA, SONG_LEN(songA)},
{"B BACKSEAT RADIO", songB, durB, SONG_LEN(songB)},
{"C CITY LIGHTS", songC, durC, SONG_LEN(songC)},
{"D DOWNTOWN", songD, durD, SONG_LEN(songD)},
{"E ELECTRIC AVE", songE, durE, SONG_LEN(songE)},
{"F FADE INTO NEON", songF, durF, SONG_LEN(songF)},
{"G GOLDEN HOUR", songG, durG, SONG_LEN(songG)},
{"H HIGHWAY STATIC", songH, durH, SONG_LEN(songH)},
{"I IN THE DARK", songI, durI, SONG_LEN(songI)},
{"J JET BLACK", songJ, durJ, SONG_LEN(songJ)},
{"K MIDNIGHT DRIVE", songK, durK, SONG_LEN(songK)},
{"L LATE NIGHT", songL, durL, SONG_LEN(songL)},
{"M MIDNIGHT CITY", songM, durM, SONG_LEN(songM)},
{"N NIGHTSHIFT", songN, durN, SONG_LEN(songN)},
{"O OPEN ROAD", songO, durO, SONG_LEN(songO)},
{"P PULSE", songP, durP, SONG_LEN(songP)},
{"Q QUIET FIRE", songQ, durQ, SONG_LEN(songQ)},
{"R REDLINE", songR, durR, SONG_LEN(songR)},
{"S STREETLIGHTS", songS, durS, SONG_LEN(songS)},
{"T TRANSMISSION", songT, durT, SONG_LEN(songT)},
{"U UNDERGROUND", songU, durU, SONG_LEN(songU)},
{"V VELVET SKY", songV, durV, SONG_LEN(songV)},
{"W WILD SIDE", songW, durW, SONG_LEN(songW)},
{"X X-RAY", songX, durX, SONG_LEN(songX)},
{"Y YOUNG AGAIN", songY, durY, SONG_LEN(songY)},
{"Z ZERO HOUR", songZ, durZ, SONG_LEN(songZ)}
};
const int MUSIC_COUNT = sizeof(musicLibrary) / sizeof(musicLibrary[0]);
int musicSelection = 0;
int musicNoteIndex = 0;
bool musicPlaying = true;
unsigned long musicNextNoteTime = 0;
unsigned long musicLastMove = 0;
// Holding the button in Music exits instead of changing playback.
#define MUSIC_EXIT_HOLD 1500
// =============================================================================
// MUSIC FUNCTIONS
// =============================================================================
void stopMusic() {
noTone(BUZZER_PIN);
musicPlaying = false;
musicNoteIndex = 0;
musicNextNoteTime = 0;
}
void startMusicSong(int index) {
if (index < 0) index = MUSIC_COUNT - 1;
if (index >= MUSIC_COUNT) index = 0;
musicSelection = index;
musicNoteIndex = 0;
musicPlaying = true;
musicNextNoteTime = 0;
noTone(BUZZER_PIN);
}
void nextMusicSong() {
musicSelection++;
if (musicSelection >= MUSIC_COUNT) {
musicSelection = 0;
}
startMusicSong(musicSelection);
}
void previousMusicSong() {
musicSelection--;
if (musicSelection < 0) {
musicSelection = MUSIC_COUNT - 1;
}
startMusicSong(musicSelection);
}
void updateMusicPlayback() {
if (!musicPlaying) {
noTone(BUZZER_PIN);
return;
}
unsigned long now = millis();
if (now < musicNextNoteTime) {
return;
}
MusicSong &song = musicLibrary[musicSelection];
if (musicNoteIndex >= song.length) {
musicNoteIndex = 0;
}
uint16_t note = song.notes[musicNoteIndex];
uint16_t duration = song.durations[musicNoteIndex];
if (note == REST) {
noTone(BUZZER_PIN);
} else {
tone(BUZZER_PIN, note);
}
musicNextNoteTime = now + duration;
musicNoteIndex++;
if (musicNoteIndex >= song.length) {
musicNoteIndex = 0;
}
}
void drawMusicScreen() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("MUSIC");
display.setCursor(94, 0);
display.print(musicPlaying ? "PLAY" : "PAUSE");
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
// Letter indicator
display.setTextSize(2);
display.setCursor(4, 18);
char letter = 'A' + musicSelection;
display.print(letter);
display.setTextSize(1);
// Song title
display.setCursor(22, 19);
display.print(musicLibrary[musicSelection].title);
display.setCursor(22, 32);
display.print("TRACK ");
display.print(musicSelection + 1);
display.print("/");
display.print(MUSIC_COUNT);
// Progress
int progressWidth = 0;
if (musicLibrary[musicSelection].length > 0) {
progressWidth =
map(musicNoteIndex,
0,
musicLibrary[musicSelection].length,
0,
118);
}
display.drawRect(4, 42, 120, 7, SSD1306_WHITE);
if (progressWidth > 2) {
display.fillRect(5, 43, progressWidth, 5, SSD1306_WHITE);
}
display.setCursor(2, 53);
display.print("< > SONG");
display.setCursor(72, 53);
display.print("BTN ");
display.print(musicPlaying ? "PAUSE" : "PLAY");
display.display();
}
void handleMusicInput() {
int xVal = readJoystickX();
int yVal = readJoystickY();
if (millis() - musicLastMove < 250) {
return;
}
if (xVal > 2800) {
nextMusicSong();
musicLastMove = millis();
return;
}
if (xVal < 1200) {
previousMusicSong();
musicLastMove = millis();
return;
}
if (yVal > 2800) {
nextMusicSong();
musicLastMove = millis();
return;
}
if (yVal < 1300) {
previousMusicSong();
musicLastMove = millis();
return;
}
}
void updateMusic() {
handleMusicInput();
updateMusicPlayback();
drawMusicScreen();
}
// =============================================================================
// SIGN-IN
// =============================================================================
void drawSignInScreen() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(16, 8);
display.print("PLEASE SIGN IN");
display.drawRect(8, 24, 112, 28, SSD1306_WHITE);
display.setCursor(18, 34);
display.print("TAP CARD TO PLAY");
display.display();
}
bool isCardPresent() {
byte bufferATQA[2];
byte bufferSize = sizeof(bufferATQA);
mfrc522.PICC_HaltA();
MFRC522::StatusCode status =
mfrc522.PICC_WakeupA(bufferATQA, &bufferSize);
return (status == MFRC522::STATUS_OK);
}
void checkCardSignIn() {
drawSignInScreen();
if (!mfrc522.PICC_IsNewCardPresent()) {
if (millis() - lastRFIDRecovery >= 60000UL) {
mfrc522.PCD_Reset();
delay(50);
mfrc522.PCD_Init();
mfrc522.PCD_AntennaOn();
delay(5);
lastRFIDRecovery = millis();
}
return;
}
if (!mfrc522.PICC_ReadCardSerial()) {
return;
}
currentPlayerUID = "";
for (byte i = 0; i < mfrc522.uid.size; i++) {
if (mfrc522.uid.uidByte[i] < 0x10) {
currentPlayerUID += "0";
}
currentPlayerUID +=
String(mfrc522.uid.uidByte[i], HEX);
}
currentPlayerUID.toUpperCase();
if (currentPlayerUID == ALLOWED_UID_1 ||
currentPlayerUID == ALLOWED_UID_2) {
unsigned long cardTouchStart = millis();
bool cheatUnlocked = false;
while (true) {
unsigned long duration =
millis() - cardTouchStart;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(18, 12);
display.print("WELCOME PLAYER!");
display.setCursor(10, 28);
display.print("ID: ");
display.print(currentPlayerUID);
if (duration >= CHEAT_GRACE_PERIOD) {
display.setCursor(8, 40);
display.print("Hold Card for Power!");
int progress =
map(
constrain(
duration,
CHEAT_GRACE_PERIOD,
CHEAT_HOLD_TIME
),
CHEAT_GRACE_PERIOD,
CHEAT_HOLD_TIME,
0,
100
);
display.drawRect(14, 52, 100, 8, SSD1306_WHITE);
display.fillRect(
14,
52,
progress,
8,
SSD1306_WHITE
);
}
display.display();
if (duration >= CHEAT_HOLD_TIME) {
cheatUnlocked = true;
break;
}
if (!isCardPresent()) {
break;
}
delay(100);
}
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
if (cheatUnlocked) {
isHardMode = true;
display.clearDisplay();
display.drawRect(
4,
4,
120,
56,
SSD1306_WHITE
);
display.setTextSize(1);
display.setCursor(8, 18);
display.print("Hard Mode");
display.setCursor(8, 32);
display.print("UNLOCKED!");
display.display();
delay(2000);
} else {
isHardMode = false;
}
currentState = STATE_MENU;
} else {
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(37, 8);
display.print("INCORRECT");
display.setCursor(10, 26);
display.print("ID: ");
display.print(currentPlayerUID);
display.setCursor(37, 44);
display.print("TRY AGAIN");
display.drawRect(
4,
2,
120,
60,
SSD1306_WHITE
);
display.display();
delay(2500);
}
}
// =============================================================================
// SNAKE
// =============================================================================
void generateFood() {
int minGridX = 0;
int maxGridX =
(OLED_WIDTH / SNAKE_SIZE) - 1;
int minGridY =
(SNAKE_TOP_OFFSET / SNAKE_SIZE);
int maxGridY =
(OLED_HEIGHT / SNAKE_SIZE) - 1;
food_x =
random(minGridX, maxGridX + 1)
* SNAKE_SIZE;
food_y =
random(minGridY, maxGridY + 1)
* SNAKE_SIZE;
for (int i = 0; i < snake_length; i++) {
if (snake_x[i] == food_x &&
snake_y[i] == food_y) {
generateFood();
return;
}
}
}
void resetSnake() {
for (int i = 0; i < MAX_SNAKE_LENGTH; i++) {
snake_x[i] = -999;
snake_y[i] = -999;
}
if (isHardMode || snakeCheatActive) {
snakeScore = 39;
snake_length = 42;
snakeCheatActive = false;
int spiral_x[42] = {
76,72,68,64,60,56,52,48,44,40,36,32,
32,32,32,32,32,32,32,32,
36,40,44,48,52,56,60,64,68,72,76,
76,76,76,76,76,76,
72,68,64,60,56
};
int spiral_y[42] = {
16,16,16,16,16,16,16,16,16,16,16,16,
20,24,28,32,36,40,44,48,
48,48,48,48,48,48,48,48,48,48,48,
44,40,36,32,28,24,
24,24,24,24,24
};
for (int i = 0; i < snake_length; i++) {
snake_x[i] = spiral_x[i];
snake_y[i] = spiral_y[i];
}
} else {
snakeScore = 0;
snake_length = 3;
snake_x[0] = 60;
snake_y[0] = 32;
snake_x[1] =
snake_x[0] - SNAKE_SIZE;
snake_y[1] = snake_y[0];
snake_x[2] =
snake_x[1] - SNAKE_SIZE;
snake_y[2] = snake_y[1];
}
current_dir = DIR_RIGHT;
next_dir = DIR_RIGHT;
queued_dir = -1;
dirChangedThisTick = false;
snakeStart = true;
snakeGameOver = false;
snakeWon = false;
snakePaused = false;
endScreenSelection = 0;
generateFood();
}
void readJoystickSnake() {
int xVal = readJoystickX();
int yVal = readJoystickY();
unsigned long now = millis();
bool xCentered =
(xVal >= 1300 && xVal <= 2700);
bool yCentered =
(yVal >= 1300 && yVal <= 2700);
if (xCentered &&
yCentered &&
now - lastTurnTimeSnake >= TURN_DEBOUNCE_MS) {
joyMovedSnake = false;
}
if (joyMovedSnake) {
if (lastFlickAxisSnake == 1 &&
!yCentered &&
xVal >= 1000 &&
xVal <= 3000) {
joyMovedSnake = false;
} else if (
lastFlickAxisSnake == 2 &&
!xCentered &&
yVal >= 1000 &&
yVal <= 3000
) {
joyMovedSnake = false;
}
}
if (joyMovedSnake) return;
int active_dir =
dirChangedThisTick
? next_dir
: current_dir;
int candidate_dir = active_dir;
bool isTurnRight =
(xVal > 2700 && yCentered) ||
(yVal < 1300 && xCentered);
bool isTurnLeft =
(xVal < 1200 && yCentered) ||
(yVal > 2800 && xCentered);
int newAxis = 0;
if (xVal > 2700 || xVal < 1200)
newAxis = 1;
if (yVal > 2800 || yVal < 1300)
newAxis = 2;
if (isTurnRight) {
candidate_dir =
(active_dir + 1) % 4;
joyMovedSnake = true;
lastTurnTimeSnake = now;
lastFlickAxisSnake = newAxis;
} else if (isTurnLeft) {
candidate_dir =
(active_dir + 3) % 4;
joyMovedSnake = true;
lastTurnTimeSnake = now;
lastFlickAxisSnake = newAxis;
}
if (
(active_dir == DIR_UP &&
candidate_dir == DIR_DOWN) ||
(active_dir == DIR_DOWN &&
candidate_dir == DIR_UP) ||
(active_dir == DIR_LEFT &&
candidate_dir == DIR_RIGHT) ||
(active_dir == DIR_RIGHT &&
candidate_dir == DIR_LEFT)
) {
return;
}
if (candidate_dir != active_dir) {
if (!dirChangedThisTick) {
next_dir = candidate_dir;
dirChangedThisTick = true;
} else {
queued_dir = candidate_dir;
}
}
}
void moveSnake() {
current_dir = next_dir;
if (queued_dir != -1) {
next_dir = queued_dir;
queued_dir = -1;
dirChangedThisTick = true;
} else {
dirChangedThisTick = false;
}
for (int i = snake_length - 1; i > 0; i--) {
snake_x[i] = snake_x[i - 1];
snake_y[i] = snake_y[i - 1];
}
if (current_dir == DIR_UP)
snake_y[0] -= SNAKE_SIZE;
if (current_dir == DIR_DOWN)
snake_y[0] += SNAKE_SIZE;
if (current_dir == DIR_LEFT)
snake_x[0] -= SNAKE_SIZE;
if (current_dir == DIR_RIGHT)
snake_x[0] += SNAKE_SIZE;
}
bool checkSnakeOverBoundary() {
if (
snake_x[0] < 0 ||
snake_x[0] + SNAKE_SIZE > OLED_WIDTH ||
snake_y[0] < SNAKE_TOP_OFFSET ||
snake_y[0] + SNAKE_SIZE > OLED_HEIGHT
) {
return true;
}
for (int i = 1; i < snake_length; i++) {
if (
snake_x[i] != -999 &&
snake_x[0] == snake_x[i] &&
snake_y[0] == snake_y[i]
) {
return true;
}
}
return false;
}
bool checkFoodCollision() {
if (
snake_x[0] == food_x &&
snake_y[0] == food_y
) {
if (snake_length < MAX_SNAKE_LENGTH) {
snake_x[snake_length] =
snake_x[snake_length - 1];
snake_y[snake_length] =
snake_y[snake_length - 1];
snake_length++;
}
return true;
}
return false;
}
void drawSnakeGame() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(2, 1);
display.print(isHardMode ? "*SNAKE*" : "SNAKE");
display.setCursor(72, 1);
display.print("SCORE:");
if (snakeScore < 10)
display.print("0");
display.print(snakeScore);
display.drawLine(
0,
10,
OLED_WIDTH,
10,
SSD1306_WHITE
);
display.fillCircle(
food_x + 1,
food_y + 1,
FOOD_SIZE / 2,
SSD1306_WHITE
);
for (int i = 0; i < snake_length; i++) {
if (snake_x[i] != -999) {
if (i == 0) {
display.fillRoundRect(
snake_x[i],
snake_y[i],
SNAKE_SIZE,
SNAKE_SIZE,
1,
SSD1306_WHITE
);
} else {
display.drawRoundRect(
snake_x[i],
snake_y[i],
SNAKE_SIZE,
SNAKE_SIZE,
1,
SSD1306_WHITE
);
}
}
}
display.display();
}
// =============================================================================
// END SCREEN
// =============================================================================
void drawEndScreen(
const char* titleText,
bool allowContinue
) {
display.clearDisplay();
int len = strlen(titleText);
int textSize =
(len > 9) ? 1 : 2;
int charWidth =
(textSize == 2) ? 12 : 6;
display.setTextSize(textSize);
int titleX =
(OLED_WIDTH - len * charWidth) / 2;
int titleY =
(textSize == 2) ? 10 : 14;
display.setCursor(titleX, titleY);
display.print(titleText);
display.setTextSize(1);
if (allowContinue) {
int contX = 10;
int homeX = 82;
int optionY = 44;
display.setCursor(contX, optionY);
display.print("CONTINUE");
display.setCursor(homeX, optionY);
display.print("HOME");
if (endScreenSelection == 0) {
display.drawLine(
contX,
optionY + 10,
contX + 47,
optionY + 10,
SSD1306_WHITE
);
} else {
display.drawLine(
homeX,
optionY + 10,
homeX + 23,
optionY + 10,
SSD1306_WHITE
);
}
} else {
int homeX =
(OLED_WIDTH - 24) / 2;
int optionY = 44;
display.setCursor(homeX, optionY);
display.print("HOME");
display.drawLine(
homeX,
optionY + 10,
homeX + 23,
optionY + 10,
SSD1306_WHITE
);
endScreenSelection = 1;
}
display.display();
}
void handleEndScreenInput(bool allowContinue) {
if (!allowContinue) {
endScreenSelection = 1;
return;
}
int xVal = readJoystickX();
int yVal = readJoystickY();
if (millis() - lastMenuMove > 200) {
if (xVal < 1200 || yVal > 2800) {
endScreenSelection = 0;
lastMenuMove = millis();
} else if (
xVal > 2800 ||
yVal < 1300
) {
endScreenSelection = 1;
lastMenuMove = millis();
}
}
}
// =============================================================================
// SNAKE UPDATE
// =============================================================================
void updateSnakeGame() {
if (snakeStart) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(
OLED_WIDTH / 2 - 51,
OLED_HEIGHT / 2 - 4
);
display.print("MOVE JOY TO START");
display.display();
int xVal = readJoystickX();
int yVal = readJoystickY();
if (
abs(xVal - restX) > 800 ||
abs(yVal - restY) > 800
) {
delay(150);
snakeStart = false;
}
return;
}
if (snakePaused) {
handleEndScreenInput(true);
drawEndScreen("GAME PAUSED", true);
return;
}
if (snakeGameOver) {
handleEndScreenInput(true);
drawEndScreen("GAME OVER", true);
return;
}
if (snakeWon) {
handleEndScreenInput(true);
drawEndScreen("YOU WIN!", true);
return;
}
readJoystickSnake();
if (
millis() - lastSnakeMoveTime >=
SNAKE_MOVE_SPEED
) {
lastSnakeMoveTime = millis();
moveSnake();
if (checkSnakeOverBoundary()) {
playGameOverSfx();
snakeGameOver = true;
endScreenSelection = 0;
}
if (checkFoodCollision()) {
generateFood();
playSfx(1250, 55);
snakeScore++;
if (snakeScore == SNAKE_WIN_SCORE) {
playWinSfx();
snakeWon = true;
endScreenSelection = 0;
}
}
drawSnakeGame();
}
}
// =============================================================================
// BREAKOUT
// =============================================================================
void initBricks() {
remainingBricks = 0;
for (int r = 0; r < currentBrickRows; r++) {
for (int c = 0; c < BRICK_COLS; c++) {
bricks[r][c] = true;
remainingBricks++;
}
}
}
void resetBreakoutBall() {
ballX =
paddleX +
PADDLE_WIDTH / 2.0 -
BALL_SIZE / 2.0;
ballY =
PADDLE_Y -
BALL_SIZE -
1;
ballDX = 1.5;
ballDY = -1.5;
}
void resetBreakoutGame() {
breakoutScore = 0;
if (isHardMode || breakoutCheatActive) {
currentBrickRows = MAX_BRICK_ROWS;
breakoutLives = 7;
breakoutScore = 1200;
breakoutCheatActive = false;
} else {
currentBrickRows =
INITIAL_BRICK_ROWS;
}
breakoutGameOver = false;
breakoutWon = false;
breakoutPaused = false;
breakoutStart = true;
paddleX =
(OLED_WIDTH - PADDLE_WIDTH) / 2.0;
endScreenSelection = 0;
initBricks();
resetBreakoutBall();
}
void readInputBreakout() {
int xVal = readJoystickX();
int yVal = readJoystickY();
int deltaX = xVal - restX;
int deltaY = yVal - restY;
int effectiveDelta =
(abs(deltaX) >= abs(deltaY))
? deltaX
: -deltaY;
if (abs(effectiveDelta) > 250) {
float rawRatio =
(float)(abs(effectiveDelta) - 250) /
(2048.0 - 250.0);
rawRatio =
constrain(rawRatio, 0.0, 1.0);
float speed =
MIN_PADDLE_SPEED +
(rawRatio * rawRatio * rawRatio) *
(MAX_PADDLE_SPEED - MIN_PADDLE_SPEED);
if (effectiveDelta > 0)
paddleX += speed;
else
paddleX -= speed;
}
if (paddleX < 0)
paddleX = 0;
if (
paddleX >
OLED_WIDTH - PADDLE_WIDTH
)
paddleX =
OLED_WIDTH - PADDLE_WIDTH;
}
void updateBreakoutPhysics() {
ballX += ballDX;
ballY += ballDY;
if (ballX <= 0) {
ballX = 0;
ballDX = -ballDX;
} else if (
ballX >= OLED_WIDTH - BALL_SIZE
) {
ballX =
OLED_WIDTH - BALL_SIZE;
ballDX = -ballDX;
}
if (ballY <= 0) {
ballY = 0;
ballDY = -ballDY;
}
if (
ballY + BALL_SIZE >= PADDLE_Y &&
ballY + BALL_SIZE <=
PADDLE_Y + PADDLE_HEIGHT &&
ballX + BALL_SIZE >= paddleX &&
ballX <= paddleX + PADDLE_WIDTH
) {
ballDY = -abs(ballDY);
playSfx(1050, 22);
float hitPoint =
(ballX + BALL_SIZE / 2.0) -
(paddleX + PADDLE_WIDTH / 2.0);
ballDX = hitPoint * 0.15;
}
for (int r = 0; r < currentBrickRows; r++) {
for (int c = 0; c < BRICK_COLS; c++) {
if (bricks[r][c]) {
int bx =
BRICK_OFFSET_LEFT +
c * (BRICK_WIDTH + BRICK_GAP);
int by =
BRICK_OFFSET_TOP +
r * (BRICK_HEIGHT + BRICK_GAP);
if (
ballX + BALL_SIZE >= bx &&
ballX <= bx + BRICK_WIDTH &&
ballY + BALL_SIZE >= by &&
ballY <= by + BRICK_HEIGHT
) {
bricks[r][c] = false;
remainingBricks--;
playSfx(
760 + (r % 4) * 130,
35
);
breakoutScore += 10;
ballDY = -ballDY;
if (remainingBricks <= 0) {
breakoutWon = true;
endScreenSelection =
(currentBrickRows >= MAX_BRICK_ROWS)
? 1
: 0;
}
return;
}
}
}
}
if (ballY > OLED_HEIGHT) {
playSfx(300, 90);
breakoutLives--;
if (breakoutLives <= 0) {
playGameOverSfx();
breakoutGameOver = true;
endScreenSelection = 0;
} else {
resetBreakoutBall();
}
}
}
void drawBreakoutGame() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("S:");
display.print(breakoutScore);
display.setCursor(
OLED_WIDTH - 24,
0
);
display.print("L:");
display.print(breakoutLives);
for (int r = 0; r < currentBrickRows; r++) {
for (int c = 0; c < BRICK_COLS; c++) {
if (bricks[r][c]) {
int bx =
BRICK_OFFSET_LEFT +
c * (BRICK_WIDTH + BRICK_GAP);
int by =
BRICK_OFFSET_TOP +
r * (BRICK_HEIGHT + BRICK_GAP);
display.fillRect(
bx,
by,
BRICK_WIDTH,
BRICK_HEIGHT,
SSD1306_WHITE
);
}
}
}
display.fillRect(
(int)paddleX,
PADDLE_Y,
PADDLE_WIDTH,
PADDLE_HEIGHT,
SSD1306_WHITE
);
display.fillRect(
(int)ballX,
(int)ballY,
BALL_SIZE,
BALL_SIZE,
SSD1306_WHITE
);
display.display();
}
void updateBreakoutGame() {
if (breakoutStart) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(
OLED_WIDTH / 2 - 51,
OLED_HEIGHT / 2 - 4
);
display.print("MOVE JOY TO START");
display.display();
int xVal = readJoystickX();
int yVal = readJoystickY();
if (
abs(xVal - restX) > 800 ||
abs(yVal - restY) > 800
) {
delay(150);
breakoutStart = false;
}
return;
}
if (breakoutPaused) {
handleEndScreenInput(true);
drawEndScreen("GAME PAUSED", true);
return;
}
if (breakoutGameOver) {
handleEndScreenInput(true);
drawEndScreen("GAME OVER", true);
return;
}
if (breakoutWon) {
bool allowContinue =
currentBrickRows < MAX_BRICK_ROWS;
handleEndScreenInput(
allowContinue
);
drawEndScreen(
"YOU WIN!",
allowContinue
);
return;
}
if (
millis() -
lastBreakoutFrameTime >=
BREAKOUT_FRAME_DELAY
) {
lastBreakoutFrameTime = millis();
readInputBreakout();
updateBreakoutPhysics();
drawBreakoutGame();
}
}
// =============================================================================
// SPACE INVADERS SPRITES
// =============================================================================
void drawAlienSprite(int x, int y) {
display.drawPixel(
x + 2,
y,
SSD1306_WHITE
);
display.drawPixel(
x + 5,
y,
SSD1306_WHITE
);
display.fillRect(
x + 1,
y + 1,
6,
2,
SSD1306_WHITE
);
display.drawPixel(
x + 2,
y + 2,
SSD1306_BLACK
);
display.drawPixel(
x + 5,
y + 2,
SSD1306_BLACK
);
display.fillRect(
x,
y + 3,
8,
2,
SSD1306_WHITE
);
display.drawPixel(
x + 1,
y + 5,
SSD1306_WHITE
);
display.drawPixel(
x + 3,
y + 5,
SSD1306_WHITE
);
display.drawPixel(
x + 4,
y + 5,
SSD1306_WHITE
);
display.drawPixel(
x + 6,
y + 5,
SSD1306_WHITE
);
}
void drawMothershipSprite(int x, int y) {
display.drawFastHLine(
x + 12,
y,
16,
SSD1306_WHITE
);
display.fillRect(
x + 6,
y + 2,
28,
4,
SSD1306_WHITE
);
display.fillRect(
x,
y + 6,
40,
3,
SSD1306_WHITE
);
display.drawPixel(
x + 8,
y + 4,
SSD1306_BLACK
);
display.drawPixel(
x + 20,
y + 4,
SSD1306_BLACK
);
display.drawPixel(
x + 31,
y + 4,
SSD1306_BLACK
);
display.fillRect(
x + 14,
y + 9,
12,
2,
SSD1306_WHITE
);
}
void initInvaders() {
for (int r = 0; r < ALIEN_ROWS; r++) {
for (int c = 0; c < ALIEN_COLS; c++) {
invaders[r][c].x =
10 + c * (ALIEN_WIDTH + ALIEN_GAP_X);
invaders[r][c].y =
12 + r * (ALIEN_HEIGHT + ALIEN_GAP_Y);
invaders[r][c].alive = true;
}
}
}
void resetInvadersGame() {
invadersScore = 0;
if (isHardMode || invadersCheatActive) {
maxPlayerHealth = 200;
invadersHealth = 200;
invadersScore = 500;
isMothershipStage = true;
mothershipHealth = 1000;
mothershipMaxHealth = 1000;
mothershipX =
(OLED_WIDTH - 40) / 2.0;
mothershipY = 16;
mothershipDir = 1;
invadersCheatActive = false;
} else {
maxPlayerHealth = 60;
invadersHealth = 60;
isMothershipStage = false;
initInvaders();
}
shipX =
(OLED_WIDTH - SHIP_WIDTH) / 2.0;
shipY =
OLED_HEIGHT - SHIP_HEIGHT - 2;
alienDirection = 1;
invadersGameOver = false;
invadersWon = false;
invadersPaused = false;
invadersStart = true;
endScreenSelection = 0;
lastInvaderMove = millis();
lastEnemyShootTime = millis();
lastMothershipShootBurst = millis();
lastInvadersMachineGunShot = millis();
for (int i = 0; i < MAX_BULLETS; i++) {
bullets[i].active = false;
}
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
enemyBullets[i].active = false;
}
}
void fireInvadersBullet() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullets[i].active) {
bullets[i].x =
shipX +
SHIP_WIDTH / 2.0 -
0.5;
bullets[i].y =
shipY - 2;
bullets[i].active = true;
playSfx(1500, 28);
break;
}
}
}
void fireEnemyBullet() {
if (!isMothershipStage) {
int aliveCount = 0;
Alien* aliveAliens[
ALIEN_ROWS * ALIEN_COLS
];
for (int r = 0; r < ALIEN_ROWS; r++) {
for (int c = 0; c < ALIEN_COLS; c++) {
if (invaders[r][c].alive) {
aliveAliens[aliveCount++] =
&invaders[r][c];
}
}
}
if (aliveCount > 0) {
Alien* shooter =
aliveAliens[random(0, aliveCount)];
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (!enemyBullets[i].active) {
enemyBullets[i].x =
shooter->x +
ALIEN_WIDTH / 2.0;
enemyBullets[i].y =
shooter->y +
ALIEN_HEIGHT;
enemyBullets[i].active = true;
playSfx(420, 55);
break;
}
}
}
} else {
barrageBulletsLeft = 6;
}
}
void readInputInvaders() {
int xVal = readJoystickX();
int yVal = readJoystickY();
if (xVal > 2800)
shipX += 2.0;
if (xVal < 1200)
shipX -= 2.0;
if (yVal > 2800)
shipY += 2.0;
if (yVal < 1200)
shipY -= 2.0;
if (shipX < 0)
shipX = 0;
if (
shipX >
OLED_WIDTH - SHIP_WIDTH
)
shipX =
OLED_WIDTH - SHIP_WIDTH;
if (shipY < OLED_HEIGHT / 2)
shipY = OLED_HEIGHT / 2;
if (
shipY >
OLED_HEIGHT - SHIP_HEIGHT
)
shipY =
OLED_HEIGHT - SHIP_HEIGHT;
}
void updateInvadersPhysics() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullets[i].active) {
bullets[i].y -= 3.5;
if (bullets[i].y < 0) {
bullets[i].active = false;
} else {
if (!isMothershipStage) {
for (int r = 0; r < ALIEN_ROWS; r++) {
for (int c = 0; c < ALIEN_COLS; c++) {
if (invaders[r][c].alive) {
if (
bullets[i].x >= invaders[r][c].x &&
bullets[i].x <=
invaders[r][c].x + ALIEN_WIDTH &&
bullets[i].y >= invaders[r][c].y &&
bullets[i].y <=
invaders[r][c].y + ALIEN_HEIGHT
) {
invaders[r][c].alive = false;
bullets[i].active = false;
playSfx(880, 45);
invadersScore += 20;
bool allDead = true;
for (int ar = 0; ar < ALIEN_ROWS; ar++) {
for (int ac = 0; ac < ALIEN_COLS; ac++) {
if (invaders[ar][ac].alive)
allDead = false;
}
}
if (allDead) {
isMothershipStage = true;
mothershipHealth = 40;
mothershipMaxHealth = 40;
mothershipX =
(OLED_WIDTH - 40) / 2.0;
mothershipY = 16;
mothershipDir = 1;
invadersScore += 200;
}
}
}
}
}
} else {
if (
bullets[i].x >= mothershipX &&
bullets[i].x <= mothershipX + 40 &&
bullets[i].y >= mothershipY &&
bullets[i].y <= mothershipY + 11
) {
bullets[i].active = false;
playSfx(660, 35);
mothershipHealth -= 1;
invadersScore += 50;
if (mothershipHealth <= 0) {
mothershipHealth = 0;
playWinSfx();
invadersWon = true;
endScreenSelection = 1;
}
}
}
}
}
}
if (!isMothershipStage) {
if (millis() - lastEnemyShootTime > 700) {
lastEnemyShootTime = millis();
fireEnemyBullet();
}
} else {
if (
millis() -
lastMothershipShootBurst > 1200
) {
lastMothershipShootBurst = millis();
fireEnemyBullet();
}
if (
barrageBulletsLeft > 0 &&
millis() - lastBarrageBulletTime > 45
) {
lastBarrageBulletTime = millis();
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (!enemyBullets[i].active) {
enemyBullets[i].x =
mothershipX +
random(4, 36);
enemyBullets[i].y =
mothershipY + 11;
enemyBullets[i].active = true;
barrageBulletsLeft--;
break;
}
}
}
}
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemyBullets[i].active) {
enemyBullets[i].y += 3.0;
if (enemyBullets[i].y > OLED_HEIGHT) {
enemyBullets[i].active = false;
} else {
if (
enemyBullets[i].x >= shipX &&
enemyBullets[i].x <= shipX + SHIP_WIDTH &&
enemyBullets[i].y >= shipY &&
enemyBullets[i].y <= shipY + SHIP_HEIGHT
) {
enemyBullets[i].active = false;
invadersHealth -= 1;
if (invadersHealth <= 0) {
invadersHealth = 0;
playGameOverSfx();
invadersGameOver = true;
endScreenSelection = 0;
}
}
}
}
}
if (!isMothershipStage) {
if (millis() - lastInvaderMove > 600) {
lastInvaderMove = millis();
bool shiftDown = false;
for (int r = 0; r < ALIEN_ROWS; r++) {
for (int c = 0; c < ALIEN_COLS; c++) {
if (invaders[r][c].alive) {
invaders[r][c].x +=
alienDirection * 3;
if (
invaders[r][c].x <= 2 ||
invaders[r][c].x >=
OLED_WIDTH - ALIEN_WIDTH - 2
) {
shiftDown = true;
}
}
}
}
if (shiftDown) {
alienDirection =
-alienDirection;
for (int r = 0; r < ALIEN_ROWS; r++) {
for (int c = 0; c < ALIEN_COLS; c++) {
invaders[r][c].y += 3;
if (
invaders[r][c].alive &&
invaders[r][c].y >= shipY - 4
) {
if (!invadersGameOver)
playGameOverSfx();
invadersGameOver = true;
endScreenSelection = 0;
}
}
}
}
}
} else {
if (millis() - lastInvaderMove > 35) {
lastInvaderMove = millis();
mothershipX +=
mothershipDir * 1.5;
if (
mothershipX <= 2 ||
mothershipX >= OLED_WIDTH - 42
) {
mothershipDir =
-mothershipDir;
}
}
}
}
void drawInvadersGame() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("S:");
display.print(invadersScore);
display.setCursor(68, 0);
display.print("HP:");
display.drawRect(
86,
1,
40,
6,
SSD1306_WHITE
);
int healthBarWidth =
map(
invadersHealth,
0,
maxPlayerHealth,
0,
38
);
display.fillRect(
87,
2,
healthBarWidth,
4,
SSD1306_WHITE
);
if (!isMothershipStage) {
for (int r = 0; r < ALIEN_ROWS; r++) {
for (int c = 0; c < ALIEN_COLS; c++) {
if (invaders[r][c].alive) {
drawAlienSprite(
(int)invaders[r][c].x,
(int)invaders[r][c].y
);
}
}
}
} else {
int mx = (int)mothershipX;
int my = (int)mothershipY;
display.drawRect(
mx + 4,
my - 6,
32,
4,
SSD1306_WHITE
);
int bossHpBar =
map(
mothershipHealth,
0,
mothershipMaxHealth,
0,
30
);
display.fillRect(
mx + 5,
my - 5,
bossHpBar,
2,
SSD1306_WHITE
);
drawMothershipSprite(mx, my);
}
int sx = (int)shipX;
int sy = (int)shipY;
display.fillTriangle(
sx + SHIP_WIDTH / 2,
sy,
sx,
sy + SHIP_HEIGHT,
sx + SHIP_WIDTH,
sy + SHIP_HEIGHT,
SSD1306_WHITE
);
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullets[i].active) {
display.fillRect(
(int)bullets[i].x,
(int)bullets[i].y,
1,
3,
SSD1306_WHITE
);
}
}
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemyBullets[i].active) {
display.fillCircle(
(int)enemyBullets[i].x,
(int)enemyBullets[i].y,
1,
SSD1306_WHITE
);
}
}
display.display();
}
void updateInvadersGame() {
if (invadersStart) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(
OLED_WIDTH / 2 - 51,
OLED_HEIGHT / 2 - 4
);
display.print("MOVE JOY TO START");
display.display();
int xVal = readJoystickX();
int yVal = readJoystickY();
if (
abs(xVal - restX) > 800 ||
abs(yVal - restY) > 800
) {
delay(150);
invadersStart = false;
}
return;
}
if (invadersPaused) {
handleEndScreenInput(true);
drawEndScreen("GAME PAUSED", true);
return;
}
if (invadersGameOver) {
handleEndScreenInput(true);
drawEndScreen("GAME OVER", true);
return;
}
if (invadersWon) {
handleEndScreenInput(false);
drawEndScreen("YOU WIN!", false);
return;
}
if (
millis() - lastInvaderFrame >=
INVADER_FRAME_DELAY
) {
lastInvaderFrame = millis();
readInputInvaders();
if (
isHardMode &&
digitalRead(JOY_SW_PIN) == LOW &&
millis() - lastInvadersMachineGunShot >=
INVADERS_MACHINE_GUN_INTERVAL
) {
lastInvadersMachineGunShot = millis();
fireInvadersBullet();
}
updateInvadersPhysics();
drawInvadersGame();
}
}
// =============================================================================
// GLIDER
// =============================================================================
void resetGliderGame() {
gliderScore = 0;
if (isHardMode || gliderCheatActive) {
gliderLives = 100;
gliderScore = 200;
gliderCheatActive = false;
} else {
gliderLives = 3;
}
turretX = 64.0;
turretAngle = 1.5708;
leftTrooperCount = 0;
rightTrooperCount = 0;
gliderGameOver = false;
gliderWon = false;
gliderPaused = false;
gliderStart = true;
endScreenSelection = 0;
lastChopperSpawn = millis();
lastGliderAutoFire = millis();
for (int i = 0; i < MAX_CHOPS; i++)
choppers[i].active = false;
for (int i = 0; i < MAX_TROOPERS; i++)
troopers[i].active = false;
for (int i = 0; i < MAX_GLIDER_SHELLS; i++)
gliderShells[i].active = false;
}
void fireGliderShell() {
for (int i = 0; i < MAX_GLIDER_SHELLS; i++) {
if (!gliderShells[i].active) {
if (isHardMode) {
gliderShells[i].x = turretX;
gliderShells[i].y = 48;
gliderShells[i].dx = 0.0;
gliderShells[i].dy = -4.0;
} else {
gliderShells[i].x =
64.0 +
8.0 * cos(turretAngle);
gliderShells[i].y =
56.0 -
8.0 * sin(turretAngle);
gliderShells[i].dx =
4.0 * cos(turretAngle);
gliderShells[i].dy =
-4.0 * sin(turretAngle);
}
gliderShells[i].active = true;
playSfx(1750, 20);
break;
}
}
}
void spawnHelicopter() {
for (int i = 0; i < MAX_CHOPS; i++) {
if (!choppers[i].active) {
choppers[i].dir =
(random(0, 2) == 0)
? 1
: -1;
choppers[i].isHuge = isHardMode;
if (choppers[i].isHuge) {
choppers[i].x =
(choppers[i].dir == 1)
? -28
: 128;
choppers[i].y = 10;
choppers[i].nextDropTime =
millis() + 100;
} else {
choppers[i].x =
(choppers[i].dir == 1)
? -16
: 128;
choppers[i].y =
random(6, 18);
choppers[i].nextDropTime =
millis() +
random(500, 2000);
}
choppers[i].active = true;
break;
}
}
}
void readInputGlider() {
int xVal = readJoystickX();
if (isHardMode) {
if (xVal > 2800)
turretX += 8.0;
if (xVal < 1200)
turretX -= 8.0;
turretX =
constrain(
turretX,
5.0,
123.0
);
} else {
if (xVal > 2800)
turretAngle -= 0.08;
if (xVal < 1200)
turretAngle += 0.08;
turretAngle =
constrain(
turretAngle,
0.0,
PI
);
}
}
void updateGliderPhysics() {
if (isHardMode) {
const unsigned long CHEAT_BULLET_INTERVAL = 30;
if (
millis() -
lastGliderAutoFire >=
CHEAT_BULLET_INTERVAL
) {
lastGliderAutoFire = millis();
fireGliderShell();
}
} else {
if (digitalRead(JOY_SW_PIN) == LOW) {
if (
millis() -
lastGliderAutoFire >=
GLIDER_AUTO_FIRE_DELAY
) {
lastGliderAutoFire = millis();
fireGliderShell();
}
}
}
unsigned long spawnDelay =
isHardMode
? 800
: 2500;
if (
millis() -
lastChopperSpawn >
spawnDelay
) {
lastChopperSpawn = millis();
spawnHelicopter();
}
for (int i = 0; i < MAX_CHOPS; i++) {
if (choppers[i].active) {
float moveSpeed =
choppers[i].isHuge
? 3.0
: 1.2;
choppers[i].x +=
choppers[i].dir *
moveSpeed;
if (
millis() >
choppers[i].nextDropTime &&
choppers[i].x > 10 &&
choppers[i].x < 115
) {
choppers[i].nextDropTime =
millis() +
(
choppers[i].isHuge
? random(120, 250)
: 4000
);
for (
int t = 0;
t < MAX_TROOPERS;
t++
) {
if (!troopers[t].active) {
float proposedTrooperX =
choppers[i].x +
(
choppers[i].isHuge
? random(4, 24)
: 6
);
troopers[t].x =
isHardMode
? constrain(
proposedTrooperX,
8.0f,
119.0f
)
: proposedTrooperX;
troopers[t].y =
choppers[i].y +
(
choppers[i].isHuge
? 8
: 4
);
troopers[t].active = true;
troopers[t].hasChute = false;
troopers[t].chuteDestroyed = false;
troopers[t].landed = false;
float cannonPosX =
isHardMode
? turretX
: 64.0;
troopers[t].side =
(troopers[t].x < cannonPosX)
? -1
: 1;
break;
}
}
}
if (
choppers[i].x < -32 ||
choppers[i].x > 140
) {
choppers[i].active = false;
}
}
}
for (int t = 0; t < MAX_TROOPERS; t++) {
if (troopers[t].active) {
if (!troopers[t].landed) {
if (
troopers[t].y > 22 &&
!troopers[t].chuteDestroyed
) {
troopers[t].hasChute = true;
}
float fallSpeed =
troopers[t].hasChute
? 0.3
: 1.8;
troopers[t].y += fallSpeed;
if (troopers[t].y >= 54) {
if (troopers[t].hasChute) {
troopers[t].y = 54;
troopers[t].landed = true;
if (troopers[t].side == -1)
leftTrooperCount++;
else
rightTrooperCount++;
if (
leftTrooperCount >= 4 ||
rightTrooperCount >= 4
) {
gliderLives = 0;
if (!gliderGameOver)
playGameOverSfx();
gliderGameOver = true;
endScreenSelection = 0;
}
} else {
troopers[t].active = false;
gliderScore += 3;
}
}
}
}
}
for (
int s = 0;
s < MAX_GLIDER_SHELLS;
s++
) {
if (gliderShells[s].active) {
gliderShells[s].x +=
gliderShells[s].dx;
gliderShells[s].y +=
gliderShells[s].dy;
if (
gliderShells[s].x < 0 ||
gliderShells[s].x > 128 ||
gliderShells[s].y < 0
) {
gliderShells[s].active = false;
continue;
}
for (
int c = 0;
c < MAX_CHOPS;
c++
) {
if (choppers[c].active) {
int chopW =
choppers[c].isHuge
? 28
: 14;
int chopH =
choppers[c].isHuge
? 10
: 6;
if (
gliderShells[s].x >=
choppers[c].x &&
gliderShells[s].x <=
choppers[c].x + chopW &&
gliderShells[s].y >=
choppers[c].y &&
gliderShells[s].y <=
choppers[c].y + chopH
) {
if (!choppers[c].isHuge) {
choppers[c].active = false;
}
gliderShells[s].active = false;
playSfx(
choppers[c].isHuge
? 520
: 920,
40
);
gliderScore +=
choppers[c].isHuge
? 25
: 10;
break;
}
}
}
if (gliderShells[s].active) {
for (
int t = 0;
t < MAX_TROOPERS;
t++
) {
if (
troopers[t].active &&
!troopers[t].landed
) {
if (
gliderShells[s].x >=
troopers[t].x - 3 &&
gliderShells[s].x <=
troopers[t].x + 5 &&
gliderShells[s].y >=
troopers[t].y - 4 &&
gliderShells[s].y <=
troopers[t].y + 6
) {
if (
troopers[t].hasChute &&
gliderShells[s].y <
troopers[t].y
) {
troopers[t].hasChute = false;
troopers[t].chuteDestroyed = true;
gliderScore += 2;
} else {
troopers[t].active = false;
gliderScore += 5;
}
gliderShells[s].active = false;
playSfx(
troopers[t].hasChute
? 600
: 980,
35
);
break;
}
}
}
}
}
}
}
void drawGliderGame() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("S:");
display.print(gliderScore);
display.setCursor(90, 0);
display.print("L:");
display.print(gliderLives);
display.drawLine(
0,
58,
128,
58,
SSD1306_WHITE
);
if (isHardMode) {
int tx = (int)turretX;
display.fillRect(
tx - 5,
54,
11,
4,
SSD1306_WHITE
);
display.fillRect(
tx - 1,
48,
3,
6,
SSD1306_WHITE
);
} else {
display.fillCircle(
64,
58,
5,
SSD1306_WHITE
);
int barrelEndX =
64 +
(int)(9.0 * cos(turretAngle));
int barrelEndY =
58 -
(int)(9.0 * sin(turretAngle));
display.drawLine(
64,
58,
barrelEndX,
barrelEndY,
SSD1306_WHITE
);
}
for (int i = 0; i < MAX_CHOPS; i++) {
if (choppers[i].active) {
int cx = (int)choppers[i].x;
int cy = (int)choppers[i].y;
if (choppers[i].isHuge) {
display.fillRect(
cx + 4,
cy + 3,
20,
6,
SSD1306_WHITE
);
display.drawLine(
cx,
cy,
cx + 28,
cy,
SSD1306_WHITE
);
display.drawRect(
cx +
(
choppers[i].dir == 1
? 0
: 22
),
cy + 4,
6,
4,
SSD1306_WHITE
);
display.drawLine(
cx +
(
choppers[i].dir == 1
? -4
: 28
),
cy + 2,
cx +
(
choppers[i].dir == 1
? 2
: 24
),
cy + 5,
SSD1306_WHITE
);
} else {
display.drawRect(
cx + 2,
cy + 2,
10,
4,
SSD1306_WHITE
);
display.drawLine(
cx,
cy,
cx + 14,
cy,
SSD1306_WHITE
);
display.drawLine(
cx +
(
choppers[i].dir == 1
? 0
: 12
),
cy + 3,
cx +
(
choppers[i].dir == 1
? -3
: 15
),
cy + 3,
SSD1306_WHITE
);
}
}
}
for (int t = 0; t < MAX_TROOPERS; t++) {
if (troopers[t].active) {
int txPos = (int)troopers[t].x;
int tyPos = (int)troopers[t].y;
display.drawPixel(
txPos,
tyPos,
SSD1306_WHITE
);
display.drawLine(
txPos,
tyPos + 1,
txPos,
tyPos + 3,
SSD1306_WHITE
);
if (
troopers[t].hasChute &&
!troopers[t].landed
) {
display.drawCircle(
txPos,
tyPos - 3,
4,
SSD1306_WHITE
);
display.drawLine(
txPos - 4,
tyPos - 3,
txPos,
tyPos,
SSD1306_WHITE
);
display.drawLine(
txPos + 4,
tyPos - 3,
txPos,
tyPos,
SSD1306_WHITE
);
}
}
}
for (int i = 0; i < leftTrooperCount; i++) {
display.fillRect(
10 + i * 4,
54,
3,
4,
SSD1306_WHITE
);
}
for (int i = 0; i < rightTrooperCount; i++) {
display.fillRect(
115 - i * 4,
54,
3,
4,
SSD1306_WHITE
);
}
for (
int s = 0;
s < MAX_GLIDER_SHELLS;
s++
) {
if (gliderShells[s].active) {
display.fillRect(
(int)gliderShells[s].x,
(int)gliderShells[s].y,
2,
3,
SSD1306_WHITE
);
}
}
display.display();
}
void updateGliderGame() {
if (gliderStart) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(
OLED_WIDTH / 2 - 51,
OLED_HEIGHT / 2 - 4
);
display.print("MOVE JOY TO START");
display.display();
int xVal = readJoystickX();
int yVal = readJoystickY();
if (
abs(xVal - restX) > 800 ||
abs(yVal - restY) > 800
) {
delay(150);
gliderStart = false;
}
return;
}
if (gliderPaused) {
handleEndScreenInput(true);
drawEndScreen("GAME PAUSED", true);
return;
}
if (gliderGameOver) {
handleEndScreenInput(true);
drawEndScreen("GAME OVER", true);
return;
}
if (
millis() - lastGliderFrame >= 30
) {
lastGliderFrame = millis();
readInputGlider();
updateGliderPhysics();
drawGliderGame();
}
}
// =============================================================================
// MENU
// =============================================================================
void drawMenu() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(
isHardMode ? 28 : 31,
1
);
display.print(
isHardMode
? "*HARD MODE*"
: "SELECT GAME"
);
display.drawLine(
0,
11,
128,
11,
SSD1306_WHITE
);
display.setCursor(8, 18);
display.print("SNAKE");
display.setCursor(72, 18);
display.print("BRICK");
display.setCursor(8, 31);
display.print("SPACE");
display.setCursor(72, 31);
display.print("GLIDER");
display.setCursor(45, 44);
display.print("MUSIC");
if (menuSelection == 0) {
display.drawLine(
8, 27,
37, 27,
SSD1306_WHITE
);
} else if (menuSelection == 1) {
display.drawLine(
72, 27,
101, 27,
SSD1306_WHITE
);
} else if (menuSelection == 2) {
display.drawLine(
8, 40,
37, 40,
SSD1306_WHITE
);
} else if (menuSelection == 3) {
display.drawLine(
72, 40,
107, 40,
SSD1306_WHITE
);
} else if (menuSelection == 4) {
display.drawLine(
45,
53,
82,
53,
SSD1306_WHITE
);
}
if (buttonBeingHeld) {
unsigned long holdDuration =
millis() - buttonPressStartTime;
if (holdDuration >= CHEAT_GRACE_PERIOD) {
if (holdDuration > CHEAT_HOLD_TIME)
holdDuration = CHEAT_HOLD_TIME;
int progressWidth =
map(
holdDuration,
CHEAT_GRACE_PERIOD,
CHEAT_HOLD_TIME,
0,
100
);
display.drawRect(
14,
55,
100,
8,
SSD1306_WHITE
);
display.fillRect(
14,
55,
progressWidth,
8,
SSD1306_WHITE
);
if (
holdDuration >=
CHEAT_HOLD_TIME
) {
display.fillRect(
10,
44,
108,
18,
SSD1306_BLACK
);
display.drawRect(
10,
44,
108,
18,
SSD1306_WHITE
);
display.setCursor(18, 49);
display.print("HARD MODE!");
}
}
}
display.display();
}
void handleMenuInput() {
if (buttonBeingHeld)
return;
int xVal = readJoystickX();
int yVal = readJoystickY();
if (millis() - lastMenuMove > 200) {
if (xVal > 2800) {
menuSelection++;
if (menuSelection >= 5)
menuSelection = 0;
lastMenuMove = millis();
} else if (xVal < 1200) {
menuSelection--;
if (menuSelection < 0)
menuSelection = 4;
lastMenuMove = millis();
} else if (yVal > 2800) {
if (menuSelection == 0)
menuSelection = 2;
else if (menuSelection == 1)
menuSelection = 3;
else if (menuSelection == 2 ||
menuSelection == 3)
menuSelection = 4;
else if (menuSelection == 4)
menuSelection = 2;
lastMenuMove = millis();
} else if (yVal < 1300) {
if (menuSelection == 2)
menuSelection = 0;
else if (menuSelection == 3)
menuSelection = 1;
else if (menuSelection == 4)
menuSelection = 0;
lastMenuMove = millis();
}
}
}
// =============================================================================
// GLOBAL BUTTON
// =============================================================================
void checkGlobalButton() {
int reading =
digitalRead(JOY_SW_PIN);
if (reading != lastButtonState) {
lastButtonDebounceTime = millis();
}
if (
millis() - lastButtonDebounceTime >
BUTTON_DEBOUNCE_DELAY
) {
if (
reading == LOW &&
!buttonBeingHeld
) {
buttonBeingHeld = true;
buttonPressStartTime = millis();
}
// -------------------------------------------------------------------------
// MUSIC SPECIAL BUTTON HANDLING
// -------------------------------------------------------------------------
if (
currentState == STATE_MUSIC &&
reading == HIGH &&
buttonBeingHeld
) {
unsigned long holdDuration =
millis() - buttonPressStartTime;
buttonBeingHeld = false;
if (holdDuration >= MUSIC_EXIT_HOLD) {
stopMusic();
currentState = STATE_MENU;
} else {
musicPlaying = !musicPlaying;
if (!musicPlaying) {
noTone(BUZZER_PIN);
} else {
musicNextNoteTime = 0;
}
}
lastButtonState = reading;
return;
}
// -------------------------------------------------------------------------
// MAIN MENU HARD MODE
// -------------------------------------------------------------------------
if (
reading == LOW &&
buttonBeingHeld &&
currentState == STATE_MENU
) {
if (
millis() - buttonPressStartTime >=
CHEAT_HOLD_TIME
) {
isHardMode = true;
buttonBeingHeld = false;
lastButtonState = reading;
return;
}
}
// -------------------------------------------------------------------------
// BUTTON RELEASE
// -------------------------------------------------------------------------
if (
reading == HIGH &&
buttonBeingHeld
) {
unsigned long holdDuration =
millis() - buttonPressStartTime;
buttonBeingHeld = false;
if (
holdDuration >=
CHEAT_GRACE_PERIOD
) {
lastButtonState = reading;
return;
}
// -----------------------------------------------------------------------
// MAIN MENU
// -----------------------------------------------------------------------
if (currentState == STATE_MENU) {
if (menuSelection == 0) {
resetSnake();
currentState = STATE_SNAKE;
} else if (menuSelection == 1) {
resetBreakoutGame();
currentState = STATE_BREAKOUT;
} else if (menuSelection == 2) {
resetInvadersGame();
currentState = STATE_INVADERS;
} else if (menuSelection == 3) {
resetGliderGame();
currentState = STATE_GLIDER;
} else if (menuSelection == 4) {
startMusicSong(musicSelection);
currentState = STATE_MUSIC;
}
}
// -----------------------------------------------------------------------
// SNAKE
// -----------------------------------------------------------------------
else if (currentState == STATE_SNAKE) {
if (snakeStart) {
currentState = STATE_MENU;
} else if (snakePaused) {
if (endScreenSelection == 0)
snakePaused = false;
else
currentState = STATE_MENU;
} else if (
snakeGameOver ||
snakeWon
) {
if (endScreenSelection == 0) {
if (snakeGameOver)
resetSnake();
else
snakeWon = false;
} else {
currentState = STATE_MENU;
}
} else {
snakePaused = true;
endScreenSelection = 0;
}
}
// -----------------------------------------------------------------------
// BREAKOUT
// -----------------------------------------------------------------------
else if (currentState == STATE_BREAKOUT) {
if (breakoutStart) {
currentState = STATE_MENU;
} else if (breakoutPaused) {
if (endScreenSelection == 0)
breakoutPaused = false;
else
currentState = STATE_MENU;
} else if (
breakoutGameOver ||
breakoutWon
) {
if (endScreenSelection == 0) {
if (breakoutGameOver) {
resetBreakoutGame();
} else if (
currentBrickRows <
MAX_BRICK_ROWS
) {
currentBrickRows++;
breakoutLives++;
breakoutWon = false;
initBricks();
resetBreakoutBall();
}
} else {
currentState = STATE_MENU;
}
} else {
breakoutPaused = true;
endScreenSelection = 0;
}
}
// -----------------------------------------------------------------------
// INVADERS
// -----------------------------------------------------------------------
else if (currentState == STATE_INVADERS) {
if (invadersStart) {
currentState = STATE_MENU;
} else if (invadersPaused) {
if (endScreenSelection == 0)
invadersPaused = false;
else
currentState = STATE_MENU;
} else if (
invadersGameOver ||
invadersWon
) {
if (endScreenSelection == 0) {
resetInvadersGame();
} else {
currentState = STATE_MENU;
}
} else {
fireInvadersBullet();
}
}
// -----------------------------------------------------------------------
// GLIDER
// -----------------------------------------------------------------------
else if (currentState == STATE_GLIDER) {
if (gliderStart) {
currentState = STATE_MENU;
} else if (gliderPaused) {
if (endScreenSelection == 0)
gliderPaused = false;
else
currentState = STATE_MENU;
} else if (
gliderGameOver ||
gliderWon
) {
if (endScreenSelection == 0) {
resetGliderGame();
} else {
currentState = STATE_MENU;
}
}
}
}
}
lastButtonState = reading;
}
// =============================================================================
// SETUP
// =============================================================================
void setup() {
pinMode(BUZZER_PIN, OUTPUT);
noTone(BUZZER_PIN);
pinMode(JOY_X_PIN, INPUT);
pinMode(JOY_Y_PIN, INPUT);
pinMode(JOY_SW_PIN, INPUT_PULLUP);
if (
!display.begin(
SSD1306_SWITCHCAPVCC,
OLED_ADDR
)
) {
while (1);
}
display.setTextColor(
SSD1306_WHITE
);
SPI.begin(
18,
19,
23,
RFID_SS_PIN
);
mfrc522.PCD_Init();
mfrc522.PCD_AntennaOn();
lastRFIDRecovery = millis();
randomSeed(
analogRead(JOY_X_PIN) ^
micros()
);
delay(300);
restX = readJoystickX();
restY = readJoystickY();
}
// =============================================================================
// MAIN LOOP
// =============================================================================
void loop() {
if (
currentState !=
STATE_SIGN_IN
) {
checkGlobalButton();
}
switch (currentState) {
case STATE_SIGN_IN:
checkCardSignIn();
break;
case STATE_MENU:
handleMenuInput();
drawMenu();
break;
case STATE_SNAKE:
updateSnakeGame();
break;
case STATE_BREAKOUT:
updateBreakoutGame();
break;
case STATE_INVADERS:
updateInvadersGame();
break;
case STATE_GLIDER:
updateGliderGame();
break;
case STATE_MUSIC:
updateMusic();
break;
}
}Loading
esp32-devkit-c-v4
esp32-devkit-c-v4
Loading
ssd1306
ssd1306
Loading
mfrc522
mfrc522