Created
June 8, 2026 10:49
-
-
Save Kwieeciol/c5fa41f685ccb9a81803b0775da27465 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <Wire.h> | |
| #include <Adafruit_GFX.h> | |
| #include <Adafruit_SSD1306.h> | |
| const int SCREEN_WIDTH = 128; | |
| const int SCREEN_HEIGHT = 64; | |
| Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); | |
| // --- PINOUT --- | |
| #define BTN_UP 2 | |
| #define BTN_DOWN 3 | |
| #define BTN_LEFT 4 | |
| #define BTN_RIGHT 5 | |
| const uint8_t PINS[4] = { BTN_UP, BTN_DOWN, BTN_LEFT, BTN_RIGHT }; | |
| bool btnCur[4]; | |
| bool btnPrev[4]; | |
| // --- MASTER MENU VARIABLES --- | |
| int gameMode = 0; // 0 = Master Menu, 1 = Snake, 2 = Flappy | |
| int menuSelection = 1; | |
| // --- BIT-COMPRESSED SNAKE VARIABLES --- | |
| #define CELL 4 | |
| #define COLS (128 / CELL) // 32 Columns | |
| #define ROWS (64 / CELL) // 16 Rows | |
| #define MAXLEN (COLS * ROWS) // 512 Max Segments | |
| struct Pt { int8_t x, y; }; | |
| Pt snakeHead; // Full 2-byte absolute coordinates for the head | |
| uint8_t snakeBody[MAXLEN / 4]; // 512 segments / 4 segments per byte = 128 bytes | |
| uint16_t snakeLen; | |
| uint8_t snakeDir, snakeWantDir; // 0=UP, 1=DOWN, 2=RIGHT, 3=LEFT | |
| Pt food; | |
| uint32_t snakeScore = 0; | |
| uint32_t snakeHiScore = 0; | |
| uint16_t snakeStepMs; | |
| uint32_t snakeLastStep; | |
| uint8_t snakeState = 0; // 0=start, 1=play, 2=dead, 3=win | |
| uint32_t snakeDeadAt; | |
| // --- FLAPPY BIRD VARIABLES --- | |
| volatile bool flapped = false; | |
| uint8_t flappyState = 0; | |
| uint32_t flappyScore = 0; | |
| uint32_t flappyHiScore = 0; | |
| uint32_t flappyDeadAt = 0; | |
| const int FLAPPY_WIDTH = 64; | |
| const int FLAPPY_HEIGHT = 128; | |
| const int BIRD_X = 16; | |
| const int BIRD_RADIUS = 2; | |
| float birdY, birdVy; | |
| const float GRAVITY = 0.18; | |
| const float FLAP_IMPULSE = -2.4; | |
| float pipeX; | |
| int pipeGapY; | |
| bool pipePassed; | |
| const int PIPE_WIDTH = 12; | |
| int PIPE_GAP_HEIGHT = 36; | |
| float PIPE_SPEED = 1.5; | |
| // ========================================== | |
| // INTERRUPT FOR FLAPPY | |
| // ========================================== | |
| void flapISR() { | |
| flapped = true; | |
| } | |
| // ========================================== | |
| // SNAKE BITSTREAM HELPER FUNCTIONS | |
| // ========================================== | |
| // Engineered by yours truly, Machanzo (Jakub) <3 | |
| // Reads 2 bits from the packed byte array for a given segment index | |
| uint8_t getSegmentDir(uint16_t idx) { | |
| uint16_t bitIdx = (idx - 1) * 2; | |
| uint16_t byteIdx = bitIdx / 8; | |
| uint8_t bitOffset = bitIdx % 8; | |
| return (snakeBody[byteIdx] >> bitOffset) & 0x03; | |
| } | |
| // Writes 2 bits into the packed byte array for a given segment index | |
| void setSegmentDir(uint16_t idx, uint8_t dir) { | |
| uint16_t bitIdx = (idx - 1) * 2; | |
| uint16_t byteIdx = bitIdx / 8; | |
| uint8_t bitOffset = bitIdx % 8; | |
| snakeBody[byteIdx] &= ~(0x03 << bitOffset); // Clear old 2 bits | |
| snakeBody[byteIdx] |= ((dir & 0x03) << bitOffset); // Inject new 2 bits | |
| } | |
| void noweJedzenie() { | |
| if (snakeLen >= MAXLEN) return; // Map full, cannot spawn food | |
| bool ok; | |
| do { | |
| ok = true; | |
| food.x = random(0, COLS); | |
| food.y = random(0, ROWS); | |
| // Check collision with head | |
| if (food.x == snakeHead.x && food.y == snakeHead.y) { ok = false; continue; } | |
| // Check collision with decoded body segments | |
| Pt temp = snakeHead; | |
| for (uint16_t i = 1; i < snakeLen; i++) { | |
| uint8_t relDir = getSegmentDir(i); | |
| if (relDir == 0) temp.y--; | |
| else if (relDir == 1) temp.y++; | |
| else if (relDir == 2) temp.x++; | |
| else if (relDir == 3) temp.x--; | |
| if (food.x == temp.x && food.y == temp.y) { ok = false; break; } | |
| } | |
| } while (!ok); | |
| } | |
| void startSnake() { | |
| snakeLen = 3; | |
| snakeDir = snakeWantDir = 2; // Start heading RIGHT | |
| snakeScore = 0; | |
| snakeStepMs = 150; | |
| snakeLastStep = millis(); | |
| snakeHead.x = COLS / 2; | |
| snakeHead.y = ROWS / 2; | |
| // Set up trailing initial segments relative to previous parts | |
| setSegmentDir(1, 3); // Segment 1 is LEFT relative to head | |
| setSegmentDir(2, 3); // Segment 2 is LEFT relative to Segment 1 | |
| noweJedzenie(); | |
| snakeState = 1; | |
| } | |
| // ========================================== | |
| // FLAPPY HELPER FUNCTIONS | |
| // ========================================== | |
| void startFlappy() { | |
| birdY = 64.0; | |
| birdVy = 0.0; | |
| pipeX = FLAPPY_WIDTH; | |
| PIPE_GAP_HEIGHT = 50; | |
| pipeGapY = 64 - PIPE_GAP_HEIGHT / 2; | |
| flappyScore = 0; | |
| pipePassed = false; | |
| flapped = false; | |
| flappyState = 1; | |
| PIPE_SPEED = 1.5; | |
| } | |
| // ========================================== | |
| // SETUP | |
| // ========================================== | |
| void setup() { | |
| Serial.begin(9600); | |
| for (uint8_t i = 0; i < 4; i++) { | |
| pinMode(PINS[i], INPUT_PULLUP); | |
| btnCur[i] = (digitalRead(PINS[i]) == LOW); | |
| btnPrev[i] = btnCur[i]; | |
| } | |
| if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { | |
| while (true); | |
| } | |
| randomSeed(analogRead(A0)); | |
| display.clearDisplay(); | |
| } | |
| // ========================================== | |
| // MAIN LOOP & STATE MACHINE | |
| // ========================================== | |
| void loop() { | |
| for (uint8_t i = 0; i < 4; i++) { | |
| btnPrev[i] = btnCur[i]; | |
| btnCur[i] = (digitalRead(PINS[i]) == LOW); | |
| } | |
| display.clearDisplay(); | |
| display.setTextColor(WHITE); | |
| if (gameMode == 0) runMasterMenu(); | |
| else if (gameMode == 1) runSnakeGame(); | |
| else if (gameMode == 2) runFlappyBird(); | |
| display.display(); | |
| delay(5); | |
| } | |
| // ========================================== | |
| // MASTER MENU | |
| // ========================================== | |
| void runMasterMenu() { | |
| display.setRotation(0); | |
| display.setTextSize(2); | |
| display.setCursor(10, 5); | |
| display.print(F("CHOOSE:")); | |
| display.setTextSize(1); | |
| if (menuSelection == 1) { | |
| display.fillRect(10, 28, 100, 11, WHITE); | |
| display.setTextColor(BLACK); | |
| } else { | |
| display.setTextColor(WHITE); | |
| } | |
| display.setCursor(12, 30); | |
| display.print(F("1. SNAKE")); | |
| if (menuSelection == 2) { | |
| display.fillRect(10, 43, 100, 11, WHITE); | |
| display.setTextColor(BLACK); | |
| } else { | |
| display.setTextColor(WHITE); | |
| } | |
| display.setCursor(12, 45); | |
| display.print(F("2. FLAP BALL")); | |
| display.setTextColor(WHITE); | |
| if (btnCur[0] && !btnPrev[0]) menuSelection = 1; // UP | |
| if (btnCur[1] && !btnPrev[1]) menuSelection = 2; // DOWN | |
| if (btnCur[3] && !btnPrev[3]) { // RIGHT selects | |
| if (menuSelection == 1) { | |
| snakeState = 0; | |
| gameMode = 1; | |
| } else { | |
| flappyState = 0; | |
| display.setRotation(1); | |
| flapped = false; | |
| attachInterrupt(digitalPinToInterrupt(BTN_UP), flapISR, FALLING); | |
| gameMode = 2; | |
| } | |
| } | |
| } | |
| // ========================================== | |
| // SNAKE GAME LOGIC (BIT-COMPRESSED) | |
| // ========================================== | |
| void runSnakeGame() { | |
| // STATE 0: START SCREEN | |
| if (snakeState == 0) { | |
| display.setTextSize(2); | |
| display.setCursor(35, 8); | |
| display.print(F("SNAKE")); | |
| display.setTextSize(1); | |
| display.setCursor(15, 32); | |
| display.print(F("Nacisnij: START")); | |
| display.setCursor(15, 42); | |
| display.print(F("LEWO: Do Menu")); | |
| display.setCursor(22, 54); | |
| display.print(F("REKORD: ")); | |
| display.print(snakeHiScore); | |
| if ((btnCur[0] && !btnPrev[0]) || (btnCur[1] && !btnPrev[1]) || (btnCur[3] && !btnPrev[3])) { | |
| startSnake(); | |
| } | |
| if (btnCur[2] && !btnPrev[2]) gameMode = 0; | |
| } | |
| // STATE 1: PLAYING | |
| else if (snakeState == 1) { | |
| // New Input handling matching modified direction indices (0=UP, 1=DOWN, 2=RIGHT, 3=LEFT) | |
| if (btnCur[0] && !btnPrev[0] && snakeWantDir != 1) snakeWantDir = 0; // UP | |
| if (btnCur[1] && !btnPrev[1] && snakeWantDir != 0) snakeWantDir = 1; // DOWN | |
| if (btnCur[2] && !btnPrev[2] && snakeWantDir != 2) snakeWantDir = 3; // LEFT | |
| if (btnCur[3] && !btnPrev[3] && snakeWantDir != 3) snakeWantDir = 2; // RIGHT | |
| if (millis() - snakeLastStep >= snakeStepMs) { | |
| snakeLastStep = millis(); | |
| snakeDir = snakeWantDir; | |
| // Predict next location of head | |
| Pt nextHead = snakeHead; | |
| if (snakeDir == 0) nextHead.y--; | |
| if (snakeDir == 1) nextHead.y++; | |
| if (snakeDir == 2) nextHead.x++; | |
| if (snakeDir == 3) nextHead.x--; | |
| // Wall hit check | |
| bool dead = (nextHead.x < 0 || nextHead.x >= COLS || nextHead.y < 0 || nextHead.y >= ROWS); | |
| // Self-collision verification via real-time decoding loop | |
| if (!dead) { | |
| Pt temp = snakeHead; | |
| for (uint16_t i = 1; i < snakeLen; i++) { | |
| uint8_t relDir = getSegmentDir(i); | |
| if (relDir == 0) temp.y--; | |
| else if (relDir == 1) temp.y++; | |
| else if (relDir == 2) temp.x++; | |
| else if (relDir == 3) temp.x--; | |
| if (temp.x == nextHead.x && temp.y == nextHead.y) { dead = true; break; } | |
| } | |
| } | |
| if (dead) { | |
| if (snakeScore > snakeHiScore) snakeHiScore = snakeScore; | |
| snakeState = 2; | |
| snakeDeadAt = millis(); | |
| } else { | |
| bool ate = (nextHead.x == food.x && nextHead.y == food.y); | |
| if (ate) { | |
| snakeLen++; | |
| snakeScore++; | |
| if (snakeScore % 5 == 0 && snakeStepMs > 60) snakeStepMs -= 10; | |
| } | |
| // Win Verification Check | |
| if (snakeLen >= MAXLEN) { | |
| if (snakeScore > snakeHiScore) snakeHiScore = snakeScore; | |
| snakeState = 3; // Jump to Win State | |
| } else { | |
| // Bitshift relative segments along bitstream sequence | |
| for (uint16_t i = snakeLen - 1; i > 1; i--) { | |
| setSegmentDir(i, getSegmentDir(i - 1)); | |
| } | |
| // Segment 1 relative placement is inversion of movement vector (snakeDir XOR 1) | |
| setSegmentDir(1, snakeDir ^ 1); | |
| snakeHead = nextHead; // Advance absolute head coordinates | |
| if (ate) noweJedzenie(); | |
| } | |
| } | |
| } | |
| // DRAW GRID ENGINE | |
| display.fillRect(food.x*CELL, food.y*CELL, CELL, CELL, WHITE); // Food | |
| display.fillRect(snakeHead.x*CELL, snakeHead.y*CELL, CELL, CELL, WHITE); // Head | |
| Pt drawPt = snakeHead; | |
| for (uint16_t i = 1; i < snakeLen; i++) { | |
| uint8_t relDir = getSegmentDir(i); | |
| if (relDir == 0) drawPt.y--; | |
| else if (relDir == 1) drawPt.y++; | |
| else if (relDir == 2) drawPt.x++; | |
| else if (relDir == 3) drawPt.x--; | |
| display.fillRect(drawPt.x*CELL, drawPt.y*CELL, CELL-1, CELL-1, WHITE); // Render segment | |
| } | |
| display.setTextSize(1); | |
| display.setCursor(0, 0); | |
| display.print(snakeScore); | |
| } | |
| // STATE 2: GAME OVER SCREEN | |
| else if (snakeState == 2) { | |
| display.setTextSize(2); | |
| display.setCursor(10, 6); | |
| display.print(F("GAME OVER")); | |
| display.setTextSize(1); | |
| display.setCursor(10, 28); | |
| display.print(F("Wynik: ")); display.print(snakeScore); | |
| display.setCursor(10, 40); | |
| display.print(F("Rekord: ")); display.print(snakeHiScore); | |
| if (millis() - snakeDeadAt > 1500) { | |
| display.setCursor(10, 52); | |
| display.print(F("Nacisnij przycisk")); | |
| for (uint8_t i = 0; i < 4; i++) { | |
| if (btnCur[i] && !btnPrev[i]) { snakeState = 0; break; } | |
| } | |
| } | |
| } | |
| // STATE 3: YOU WIN SCREEN | |
| else if (snakeState == 3) { | |
| display.setTextSize(2); | |
| display.setCursor(15, 6); | |
| display.print(F("YOU WIN!")); | |
| display.setTextSize(1); | |
| display.setCursor(10, 28); | |
| display.print(F("Perfect Score!")); | |
| display.setCursor(10, 40); | |
| display.print(F("Total: ")); display.print(snakeScore); | |
| display.setCursor(10, 52); | |
| display.print(F("Press to Menu")); | |
| for (uint8_t i = 0; i < 4; i++) { | |
| if (btnCur[i] && !btnPrev[i]) { snakeState = 0; break; } | |
| } | |
| } | |
| delay(15); | |
| } | |
| // ========================================== | |
| // FLAPPY BIRD GAME LOGIC | |
| // ========================================== | |
| void runFlappyBird() { | |
| if (flappyState == 0) { | |
| display.setTextSize(2); | |
| display.setCursor(8, 20); | |
| display.print(F("FLAP")); | |
| display.setCursor(8, 40); | |
| display.print(F("BALL")); | |
| display.setTextSize(1); | |
| display.setCursor(6, 65); | |
| display.print(F("UP: Play")); | |
| display.setCursor(2, 80); | |
| display.print(F("LEFT: Exit")); | |
| display.setCursor(4, 100); | |
| display.print(F("BEST: ")); | |
| display.print(flappyHiScore); | |
| if (flapped) startFlappy(); | |
| if (digitalRead(BTN_LEFT) == LOW) { | |
| detachInterrupt(digitalPinToInterrupt(BTN_UP)); | |
| flapped = false; | |
| gameMode = 0; | |
| } | |
| } | |
| else if (flappyState == 1) { | |
| if (flapped) { | |
| birdVy = FLAP_IMPULSE; | |
| flapped = false; | |
| } | |
| birdVy += GRAVITY; | |
| birdY += birdVy; | |
| pipeX -= PIPE_SPEED; | |
| if (pipeX < -PIPE_WIDTH) { | |
| pipeX = FLAPPY_WIDTH; | |
| pipeGapY = random(10, FLAPPY_HEIGHT - 10 - PIPE_GAP_HEIGHT); | |
| pipePassed = false; | |
| } | |
| if (pipeX + PIPE_WIDTH < BIRD_X && !pipePassed) { | |
| flappyScore++; | |
| pipePassed = true; | |
| PIPE_SPEED = 1.5 + (static_cast<float>(flappyScore) / 64.0); | |
| PIPE_GAP_HEIGHT = flappyScore > 240 ? 24 : 48 - flappyScore / 20; | |
| } | |
| bool hitGroundOrCeiling = (birdY - BIRD_RADIUS <= 0 || birdY + BIRD_RADIUS >= FLAPPY_HEIGHT); | |
| bool hitPipe = false; | |
| if (BIRD_X + BIRD_RADIUS > pipeX && BIRD_X - BIRD_RADIUS < pipeX + PIPE_WIDTH) { | |
| if (birdY - BIRD_RADIUS < pipeGapY || birdY + BIRD_RADIUS > pipeGapY + PIPE_GAP_HEIGHT) { | |
| hitPipe = true; | |
| } | |
| } | |
| if (hitGroundOrCeiling || hitPipe) { | |
| if (flappyScore > flappyHiScore) flappyHiScore = flappyScore; | |
| flappyState = 2; | |
| flappyDeadAt = millis(); | |
| flapped = false; | |
| } | |
| display.fillCircle(BIRD_X, (int)birdY, BIRD_RADIUS, WHITE); | |
| display.fillRect((int)pipeX, 0, PIPE_WIDTH, pipeGapY, WHITE); | |
| display.fillRect((int)pipeX, pipeGapY + PIPE_GAP_HEIGHT, PIPE_WIDTH, FLAPPY_HEIGHT - (pipeGapY + PIPE_GAP_HEIGHT), WHITE); | |
| display.setTextSize(1); | |
| display.setCursor(0, 0); | |
| display.print(flappyScore); | |
| display.setCursor(0,8); | |
| display.print(PIPE_SPEED); | |
| display.setCursor(0,16); | |
| display.print(PIPE_GAP_HEIGHT); | |
| } | |
| else if (flappyState == 2) { | |
| display.setTextSize(2); | |
| display.setCursor(8, 20); | |
| display.print(F("GAME")); | |
| display.setCursor(8, 40); | |
| display.print(F("OVER")); | |
| display.setTextSize(1); | |
| display.setCursor(8, 70); | |
| display.print(F("Score:")); | |
| display.print(flappyScore); | |
| display.setCursor(8, 85); | |
| display.print(F("Best: ")); | |
| display.print(flappyHiScore); | |
| if (millis() - flappyDeadAt > 1000) { | |
| if (flappyScore >= flappyHiScore && flappyScore > 0) { | |
| display.setCursor(2, 105); | |
| display.print(F("NEW RECORD")); | |
| } else { | |
| display.setCursor(8, 105); | |
| display.print(F("UP: Play")); | |
| display.setCursor(3, 115); | |
| display.print(F("LEFT: Exit")); | |
| } | |
| if (flapped) { | |
| flappyState = 0; | |
| flapped = false; | |
| } | |
| if (digitalRead(BTN_LEFT) == LOW) { | |
| detachInterrupt(digitalPinToInterrupt(BTN_UP)); | |
| flapped = false; | |
| gameMode = 0; | |
| } | |
| } else { | |
| flapped = false; | |
| } | |
| } | |
| // delay(5); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment