Community project

Tetris Snake Menu

Kacper Kobrzyński

Published July 16, 2026 · Updated July 16, 2026

Arduino3 components4 assembly steps
Remix this project
Photo of Tetris Snake Menu

This project brings multiple classic games to a compact OLED display controlled by a joystick and menu button. Build a handheld game console featuring Tetris, Snake, Flappy Bird, a car dodger game, and a space shooter—all running on an Arduino Uno with an SH1106 128x64 OLED screen.

The guide includes a complete wiring diagram showing how to connect the KY-023 joystick module and push buttons to the Arduino, a full parts list, and step-by-step assembly instructions. Upload the provided firmware to add all five games with intuitive menu navigation using the joystick and menu button.

Wiring diagram

Interactive · read-only
Wiring diagram for Tetris Snake Menu

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
SH1106 OLED128x6411.3 inch 128x64 OLED display with I2C interface using the SH1106 controller
KY-023 Dual Axis Joystick ModuleKY-0231Dual-axis analog joystick breakout (PSP/PS2-style thumbstick) with two perpendicular 10 kOhm potentiometers and an integrated push-button. Outputs analog voltages on VRx and VRy proportional to stick position, plus an active-low digital switch (SW) that is pulled LOW when the stick is pressed. Operates 3.3 V to 5 V; on 5 V boards the VRx/VRy swing matches the wider ADC range. SW should be read with INPUT_PULLUP.
Push ButtonMenu button1Momentary push button switch

Assembly

4 steps
  1. Odłącz zasilanie

    Odłącz Arduino Uno od USB przed zmianą przewodów.

    • Nie wkładaj ani nie wyjmuj przewodów przy podłączonym USB.
  2. Podłącz wyświetlacz OLED

    Połącz VCC OLED z 5V Arduino, GND z GND, SDA z A4 oraz SCL z A5.

    • Tip: A4 i A5 to piny I2C Arduino Uno.
    • Sprawdź opisy na module — nie zamieniaj SDA z SCL.
  3. Podłącz joystick

    Połącz VCC joysticka z 5V, GND z GND, VRx z A0, VRy z A1, a SW z D2.

    • Tip: Joystick ma wspólną masę z Arduino.
  4. Podłącz przycisk Menu

    Podłącz jeden styk zwykłego przycisku do GND, a drugi do D3 Arduino Uno.

    • Tip: Nie potrzebujesz zewnętrznego rezystora: program używa wewnętrznego podciągania wejścia.
    • Przycisk ma zwierać D3 do GND tylko w chwili naciśnięcia.

Pin assignments

Board wiring reference
PinConnectionType
5Voled VCCpower
GNDoled GNDground
GPIO 18oled SDAi2c
GPIO 19oled SCLi2c
5Vjoystick VCCpower
GNDjoystick GNDground
GPIO 14joystick VRxadc
GPIO 15joystick VRyadc
GPIO 2joystick SWdigital
GNDmenu_button GNDground
GPIO 3menu_button SIGNALdigital

Firmware

Arduino
firmware.cppDeploy to device
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

#define JOY_X_PIN A0
#define JOY_Y_PIN A1
#define JOY_BUTTON_PIN 2
#define MENU_BUTTON_PIN 3


// Forward declarations

// Forward declarations
bool snakeContains(byte cell, byte lengthToCheck);
void placeSnakeFood();

bool joystickPressed();
bool menuButtonPressed();
uint16_t pieceMask(byte type, byte rotation);
bool blockAt(byte type, byte rotation, byte x, byte y);
bool fits(int testX, int testY, byte testRot);
void drawCell(int x, int y, bool filled);
void drawTetris();
void spawnPiece();
void lockPiece();
void startTetris();
void drawMenu();
void drawBird();
void startBird();
void updateBird();
void drawCar();
void startCar();
void updateCar();
void drawSpace();
void startSpace();
void updateSpace();
void startSnake();
void updateSnake();
void drawSnake();

const byte BOARD_W = 10;
const byte BOARD_H = 16;
const byte CELL = 4;
const byte BOARD_X = 44;
const byte BOARD_Y = 0;

Adafruit_SH1106G display(128, 64, &Wire, -1);

// Each tetromino is four 4x4 rotation bitmaps, stored in flash memory.
const uint16_t tetromino[7][4] PROGMEM = {
  {0x0F00, 0x2222, 0x00F0, 0x4444}, // I
  {0x0660, 0x0660, 0x0660, 0x0660}, // O
  {0x0E40, 0x4C40, 0x4E00, 0x4640}, // T
  {0x06C0, 0x8C40, 0x06C0, 0x8C40}, // S
  {0x0C60, 0x4C80, 0x0C60, 0x4C80}, // Z
  {0x0E20, 0x44C0, 0x08E0, 0xC440}, // J
  {0x0E80, 0xC440, 0x02E0, 0x4460}  // L
};

byte selectedItem = 0;
byte boardCells[BOARD_H];
byte pieceType, pieceRot;
int pieceX, pieceY;
unsigned int score = 0;
unsigned long lastMoveMs = 0;
unsigned long lastButtonMs = 0;
unsigned long lastMenuButtonMs = 0;
unsigned long lastDropMs = 0;
bool inTetris = false;
bool inSnake = false;
bool inBird = false;
bool inCar = false;
bool inSpace = false;
bool gameOver = false;

const byte SNAKE_W = 18;
const byte SNAKE_H = 8;
const byte SNAKE_CELL = 7;
const byte SNAKE_MAX = SNAKE_W * SNAKE_H;
byte snakeBody[SNAKE_MAX];
byte snakeLength = 0;
byte snakeFood = 0;
int8_t snakeDx = 1;
int8_t snakeDy = 0;
unsigned long lastSnakeMs = 0;
unsigned int snakeScore = 0;
bool snakeOver = false;

const byte BIRD_X = 28;
const byte BIRD_SIZE = 6;
int birdY = 29;
int birdVelocity = 0;
int pipeX = 128;
byte pipeGapY = 24;
byte birdScore = 0;
bool birdOver = false;
unsigned long lastBirdMs = 0;

const byte CAR_LEFT_LANE = 43;
const byte CAR_RIGHT_LANE = 73;
const byte CAR_WIDTH = 12;
const byte CAR_HEIGHT = 8;
byte playerCarX = CAR_LEFT_LANE;
byte enemyCarX = CAR_RIGHT_LANE;
int enemyCarY = 11;
unsigned int carScore = 0;
bool carOver = false;
unsigned long lastCarMs = 0;

const byte SPACE_LEFT = 28;
const byte SPACE_RIGHT = 100;
const byte SPACE_SHIP_Y = 55;
byte spaceShipX = 59;
int spaceEnemyX = 18;
int spaceEnemyY = 12;
int spaceShotX = 0;
int spaceShotY = -1;
unsigned int spaceScore = 0;
byte spaceEnemyHits = 1;
bool spaceArmoredEnemy = false;
bool spaceOver = false;
unsigned long lastSpaceMs = 0;
unsigned long lastSpaceMoveMs = 0;

bool joystickPressed() {
  if (digitalRead(JOY_BUTTON_PIN) == LOW && millis() - lastButtonMs > 220) {
    lastButtonMs = millis();
    return true;
  }
  return false;
}

bool menuButtonPressed() {
  if (digitalRead(MENU_BUTTON_PIN) == LOW && millis() - lastMenuButtonMs > 220) {
    lastMenuButtonMs = millis();
    return true;
  }
  return false;
}

uint16_t pieceMask(byte type, byte rotation) {
  return pgm_read_word(&tetromino[type][rotation & 3]);
}

bool blockAt(byte type, byte rotation, byte x, byte y) {
  return (pieceMask(type, rotation) & (uint16_t(1) << (15 - (y * 4 + x)))) != 0;
}

bool fits(int testX, int testY, byte testRot) {
  for (byte y = 0; y < 4; y++) {
    for (byte x = 0; x < 4; x++) {
      if (!blockAt(pieceType, testRot, x, y)) continue;
      int bx = testX + x;
      int by = testY + y;
      if (bx < 0 || bx >= BOARD_W || by >= BOARD_H) return false;
      if (by >= 0 && (boardCells[by] & (1 << bx))) return false;
    }
  }
  return true;
}

void drawCell(int x, int y, bool filled) {
  int px = BOARD_X + x * CELL;
  int py = BOARD_Y + y * CELL;
  if (filled) {
    display.fillRect(px + 1, py + 1, CELL - 1, CELL - 1, SH110X_WHITE);
  }
}

void drawTetris() {
  display.clearDisplay();
  display.drawRect(BOARD_X - 1, BOARD_Y, BOARD_W * CELL + 2, BOARD_H * CELL, SH110X_WHITE);
  for (byte y = 0; y < BOARD_H; y++) {
    for (byte x = 0; x < BOARD_W; x++) drawCell(x, y, boardCells[y] & (1 << x));
  }
  if (!gameOver) {
    for (byte y = 0; y < 4; y++) {
      for (byte x = 0; x < 4; x++) {
        if (blockAt(pieceType, pieceRot, x, y) && pieceY + y >= 0) drawCell(pieceX + x, pieceY + y, true);
      }
    }
  }
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 2);
  display.print(F("TETRIS"));
  display.setCursor(0, 13);
  display.print(F("Punkty"));
  display.setCursor(0, 23);
  display.print(score);
  display.setCursor(76, 52);
  display.print(F("gora=OBROT"));
  display.setCursor(76, 58);
  display.print(F("przycisk=DROP"));
  if (gameOver) {
    display.fillRect(10, 21, 108, 23, SH110X_BLACK);
    display.drawRect(10, 21, 108, 23, SH110X_WHITE);
    display.setCursor(27, 25);
    display.print(F("KONIEC GRY"));
    display.setCursor(15, 35);
    display.print(F("Przycisk: menu"));
  }
  display.display();
}

void spawnPiece() {
  pieceType = random(0, 7);
  pieceRot = 0;
  pieceX = 3;
  pieceY = -1;
  if (!fits(pieceX, pieceY, pieceRot)) gameOver = true;
}

void lockPiece() {
  for (byte y = 0; y < 4; y++) {
    for (byte x = 0; x < 4; x++) {
      if (blockAt(pieceType, pieceRot, x, y) && pieceY + y >= 0) boardCells[pieceY + y] |= (1 << (pieceX + x));
    }
  }
  byte cleared = 0;
  for (int y = BOARD_H - 1; y >= 0; y--) {
    if (boardCells[y] == 0x03FF) {
      for (int row = y; row > 0; row--) boardCells[row] = boardCells[row - 1];
      boardCells[0] = 0;
      cleared++;
      y++;
    }
  }
  score += cleared == 0 ? 0 : (cleared * cleared * 10);
  spawnPiece();
}

void startTetris() {
  memset(boardCells, 0, sizeof(boardCells));
  score = 0;
  gameOver = false;
  inTetris = true;
  lastDropMs = millis();
  spawnPiece();
  drawTetris();
}

void drawMenu() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(25, 0);
  display.print(F("WYBIERZ GRE"));
  display.drawFastHLine(8, 10, 112, SH110X_WHITE);
  const char *items[] = {"TETRIS", "SNAKE", "BIRD", "CAR", "SPACE"};
  for (byte i = 0; i < 5; i++) {
    int y = 12 + i * 10;
    if (i == selectedItem) {
      display.fillRoundRect(15, y - 1, 98, 10, 3, SH110X_WHITE);
      display.setTextColor(SH110X_BLACK);
    } else display.setTextColor(SH110X_WHITE);
    display.setCursor(30, y + 1);
    display.print(i == selectedItem ? '>' : ' ');
    display.print(items[i]);
  }
  display.setTextColor(SH110X_WHITE);
  display.display();
}

void drawSpace() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(1, 0);
  display.print(F("SPACE "));
  display.print(spaceScore);
  display.drawFastHLine(SPACE_LEFT, 9, SPACE_RIGHT - SPACE_LEFT, SH110X_WHITE);
  display.drawFastVLine(SPACE_LEFT, 9, 55, SH110X_WHITE);
  display.drawFastVLine(SPACE_RIGHT, 9, 55, SH110X_WHITE);

  // Armored ships appear at every 10th point; each 10 points adds one required hit.
  if (!spaceOver) {
    display.drawFastHLine(spaceEnemyX, spaceEnemyY + 2, 13, SH110X_WHITE);
    display.drawPixel(spaceEnemyX + 2, spaceEnemyY + 1, SH110X_WHITE);
    display.drawPixel(spaceEnemyX + 10, spaceEnemyY + 1, SH110X_WHITE);
    display.drawFastHLine(spaceEnemyX + 5, spaceEnemyY + 4, 3, SH110X_WHITE);
    if (spaceArmoredEnemy) {
      display.drawRect(spaceEnemyX - 1, spaceEnemyY - 1, 15, 7, SH110X_WHITE);
      display.setCursor(spaceEnemyX + 4, spaceEnemyY - 8);
      display.print(spaceEnemyHits);
    }
  }
  // Player ship
  display.fillTriangle(spaceShipX, SPACE_SHIP_Y + 7, spaceShipX + 6, SPACE_SHIP_Y, spaceShipX + 12, SPACE_SHIP_Y + 7, SH110X_WHITE);
  display.drawFastHLine(spaceShipX + 4, SPACE_SHIP_Y + 8, 5, SH110X_WHITE);
  if (spaceShotY >= 10) display.drawFastVLine(spaceShotX, spaceShotY, 4, SH110X_WHITE);

  display.setCursor(98, 53);
  display.print(F("MENU"));
  if (spaceOver) {
    display.fillRect(14, 22, 100, 22, SH110X_BLACK);
    display.drawRect(14, 22, 100, 22, SH110X_WHITE);
    display.setCursor(30, 26);
    display.print(F("KONIEC GRY"));
    display.setCursor(22, 35);
    display.print(F("Joystick: nowa"));
  }
  display.display();
}

void startSpace() {
  inSpace = true;
  spaceShipX = 59;
  spaceEnemyX = random(SPACE_LEFT + 1, SPACE_RIGHT - 13);
  spaceEnemyY = 12;
  spaceShotY = -1;
  spaceScore = 0;
  spaceEnemyHits = 1;
  spaceArmoredEnemy = false;
  spaceOver = false;
  lastSpaceMs = millis();
  lastSpaceMoveMs = millis();
  drawSpace();
}

void updateSpace() {
  int horizontal = analogRead(JOY_X_PIN);
  if (millis() - lastSpaceMoveMs > 90) {
    if (horizontal < 350 && spaceShipX > SPACE_LEFT + 1) {
      spaceShipX -= 3;
      lastSpaceMoveMs = millis();
    } else if (horizontal > 700 && spaceShipX < SPACE_RIGHT - 13) {
      spaceShipX += 3;
      lastSpaceMoveMs = millis();
    }
  }
  if (joystickPressed() && spaceShotY < 10) {
    spaceShotX = spaceShipX + 6;
    spaceShotY = SPACE_SHIP_Y - 2;
  }
  if (millis() - lastSpaceMs < 70) return;
  lastSpaceMs = millis();
  if (spaceShotY >= 10) spaceShotY -= 5;
  spaceEnemyY += 1;

  bool hitEnemy = (spaceShotY >= spaceEnemyY && spaceShotY <= spaceEnemyY + 5 &&
                   spaceShotX >= spaceEnemyX && spaceShotX <= spaceEnemyX + 12);
  if (hitEnemy) {
    spaceShotY = -1;
    spaceEnemyHits--;
    if (spaceEnemyHits == 0) {
      spaceScore++;
      spaceArmoredEnemy = (spaceScore > 0 && spaceScore % 10 == 0);
      // At 10 points it needs 2 hits, at 20 points 3 hits, and so on.
      spaceEnemyHits = spaceArmoredEnemy ? (spaceScore / 10 + 1) : 1;
      // Special armored enemies always enter at the center of the playfield.
      spaceEnemyX = spaceArmoredEnemy ? ((SPACE_LEFT + SPACE_RIGHT - 13) / 2) : random(SPACE_LEFT + 1, SPACE_RIGHT - 13);
      spaceEnemyY = 12;
    }
  }
  if (spaceEnemyY > 51) spaceOver = true;
  if (spaceShotY < 10) spaceShotY = -1;
  drawSpace();
}

void drawCar() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(1, 0);
  display.print(F("CAR  "));
  display.print(carScore);
  display.drawRect(34, 8, 60, 56, SH110X_WHITE);
  for (byte y = 13; y < 62; y += 12) {
    display.drawFastVLine(63, y, 6, SH110X_WHITE);
  }
  display.fillRoundRect(playerCarX, 53, CAR_WIDTH, CAR_HEIGHT, 2, SH110X_WHITE);
  display.drawPixel(playerCarX + 3, 55, SH110X_BLACK);
  display.drawPixel(playerCarX + 8, 55, SH110X_BLACK);
  if (!carOver) {
    display.fillRoundRect(enemyCarX, enemyCarY, CAR_WIDTH, CAR_HEIGHT, 2, SH110X_WHITE);
    display.drawPixel(enemyCarX + 3, enemyCarY + 5, SH110X_BLACK);
    display.drawPixel(enemyCarX + 8, enemyCarY + 5, SH110X_BLACK);
  }
  display.setCursor(98, 18);
  display.print(F("lewo"));
  display.setCursor(98, 27);
  display.print(F("prawo"));
  display.setCursor(98, 53);
  display.print(F("MENU"));
  if (carOver) {
    display.fillRect(6, 22, 116, 21, SH110X_BLACK);
    display.drawRect(6, 22, 116, 21, SH110X_WHITE);
    display.setCursor(25, 26);
    display.print(F("KONIEC GRY"));
    display.setCursor(13, 35);
    display.print(F("Joystick: nowa"));
  }
  display.display();
}

void startCar() {
  inCar = true;
  playerCarX = CAR_LEFT_LANE;
  enemyCarX = CAR_RIGHT_LANE;
  enemyCarY = 11;
  carScore = 0;
  carOver = false;
  lastCarMs = millis();
  drawCar();
}

void updateCar() {
  int horizontal = analogRead(JOY_X_PIN);
  if (millis() - lastMoveMs > 180) {
    if (horizontal < 350) {
      playerCarX = CAR_LEFT_LANE;
      lastMoveMs = millis();
    } else if (horizontal > 700) {
      playerCarX = CAR_RIGHT_LANE;
      lastMoveMs = millis();
    }
  }
  // Every 5 points the update interval gets 12 ms shorter, making traffic faster.
  unsigned long carInterval = 95 - min((carScore / 5) * 12, 48U);
  if (millis() - lastCarMs < carInterval) return;
  lastCarMs = millis();
  enemyCarY += 3;
  if (enemyCarY > 64) {
    enemyCarY = 11;
    enemyCarX = random(0, 2) ? CAR_LEFT_LANE : CAR_RIGHT_LANE;
    carScore++;
  }
  if (enemyCarX == playerCarX && enemyCarY + CAR_HEIGHT >= 53 && enemyCarY <= 60) carOver = true;
  drawCar();
}

void drawBird() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(1, 0);
  display.print(F("BIRD  "));
  display.print(birdScore);
  display.drawFastHLine(0, 9, 128, SH110X_WHITE);
  display.drawFastHLine(0, 63, 128, SH110X_WHITE);

  // Pipes leave a wide 27-pixel opening to make the game easier.
  display.fillRect(pipeX, 10, 11, pipeGapY - 10, SH110X_WHITE);
  display.fillRect(pipeX, pipeGapY + 27, 11, 63 - (pipeGapY + 27), SH110X_WHITE);
  display.fillCircle(BIRD_X + 3, birdY + 3, 3, SH110X_WHITE);
  display.drawPixel(BIRD_X + 5, birdY + 2, SH110X_BLACK);

  if (birdOver) {
    display.fillRect(17, 23, 94, 21, SH110X_BLACK);
    display.drawRect(17, 23, 94, 21, SH110X_WHITE);
    display.setCursor(31, 27);
    display.print(F("KONIEC GRY"));
    display.setCursor(22, 36);
    display.print(F("Joystick: nowa"));
  }
  display.display();
}

void startBird() {
  inBird = true;
  birdY = 29;
  birdVelocity = 0;
  pipeX = 128;
  pipeGapY = random(12, 28);
  birdScore = 0;
  birdOver = false;
  lastBirdMs = millis();
  drawBird();
}

void updateBird() {
  if (joystickPressed()) birdVelocity = -5;
  if (millis() - lastBirdMs < 90) return;
  lastBirdMs = millis();
  birdVelocity += 1;
  if (birdVelocity > 3) birdVelocity = 3;
  birdY += birdVelocity;
  pipeX -= 2;
  if (pipeX < -11) {
    pipeX = 128;
    pipeGapY = random(12, 28);
    birdScore++;
  }

  bool hitsPipe = (BIRD_X + BIRD_SIZE > pipeX && BIRD_X < pipeX + 11 &&
                   (birdY < pipeGapY || birdY + BIRD_SIZE > pipeGapY + 27));
  if (birdY < 10 || birdY + BIRD_SIZE > 63 || hitsPipe) birdOver = true;
  drawBird();
}

bool snakeContains(byte cell, byte lengthToCheck) {
  for (byte i = 0; i < lengthToCheck; i++) {
    if (snakeBody[i] == cell) return true;
  }
  return false;
}

void placeSnakeFood() {
  do {
    snakeFood = random(0, SNAKE_MAX);
  } while (snakeContains(snakeFood, snakeLength));
}

void drawSnake() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(1, 0);
  display.print(F("SNAKE  "));
  display.print(snakeScore);
  display.drawRect(0, 7, 128, 57, SH110X_WHITE);

  int foodX = snakeFood % SNAKE_W;
  int foodY = snakeFood / SNAKE_W;
  display.fillRect(2 + foodX * SNAKE_CELL, 9 + foodY * SNAKE_CELL, 4, 4, SH110X_WHITE);
  for (byte i = 0; i < snakeLength; i++) {
    int x = snakeBody[i] % SNAKE_W;
    int y = snakeBody[i] / SNAKE_W;
    if (i == 0) display.fillRect(1 + x * SNAKE_CELL, 8 + y * SNAKE_CELL, 6, 6, SH110X_WHITE);
    else display.drawRect(1 + x * SNAKE_CELL, 8 + y * SNAKE_CELL, 6, 6, SH110X_WHITE);
  }
  if (snakeOver) {
    display.fillRect(16, 22, 96, 22, SH110X_BLACK);
    display.drawRect(16, 22, 96, 22, SH110X_WHITE);
    display.setCursor(30, 26);
    display.print(F("KONIEC GRY"));
    display.setCursor(22, 35);
    display.print(F("Joystick: nowa"));
  }
  display.display();
}

void startSnake() {
  inSnake = true;
  snakeOver = false;
  snakeScore = 0;
  snakeLength = 3;
  snakeBody[0] = 7 + 4 * SNAKE_W;
  snakeBody[1] = 6 + 4 * SNAKE_W;
  snakeBody[2] = 5 + 4 * SNAKE_W;
  snakeDx = 1;
  snakeDy = 0;
  placeSnakeFood();
  lastSnakeMs = millis();
  drawSnake();
}

void updateSnake() {
  int horizontal = analogRead(JOY_X_PIN);
  int vertical = analogRead(JOY_Y_PIN);
  if (horizontal < 350 && snakeDx != 1) { snakeDx = -1; snakeDy = 0; }
  else if (horizontal > 700 && snakeDx != -1) { snakeDx = 1; snakeDy = 0; }
  else if (vertical < 350 && snakeDy != 1) { snakeDx = 0; snakeDy = -1; }
  else if (vertical > 700 && snakeDy != -1) { snakeDx = 0; snakeDy = 1; }

  if (millis() - lastSnakeMs < 210) return;
  lastSnakeMs = millis();
  int headX = snakeBody[0] % SNAKE_W;
  int headY = snakeBody[0] / SNAKE_W;
  int nextX = headX + snakeDx;
  int nextY = headY + snakeDy;
  if (nextX < 0 || nextX >= SNAKE_W || nextY < 0 || nextY >= SNAKE_H) {
    snakeOver = true;
    drawSnake();
    return;
  }
  byte nextCell = nextX + nextY * SNAKE_W;
  bool grows = (nextCell == snakeFood);
  if (snakeContains(nextCell, grows ? snakeLength : snakeLength - 1)) {
    snakeOver = true;
    drawSnake();
    return;
  }
  byte newLength = snakeLength + (grows && snakeLength < SNAKE_MAX ? 1 : 0);
  for (int i = newLength - 1; i > 0; i--) snakeBody[i] = snakeBody[i - 1];
  snakeBody[0] = nextCell;
  snakeLength = newLength;
  if (grows) {
    snakeScore++;
    if (snakeLength < SNAKE_MAX) placeSnakeFood();
  }
  drawSnake();
}

void setup() {
  pinMode(JOY_BUTTON_PIN, INPUT_PULLUP);
  pinMode(MENU_BUTTON_PIN, INPUT_PULLUP);
  Wire.begin();
  display.begin(0x3C, true);
  randomSeed(analogRead(A3));
  drawMenu();
}

void loop() {
  if (inTetris) {
    if (menuButtonPressed()) {
      inTetris = false;
      gameOver = false;
      drawMenu();
      return;
    }
    if (gameOver) {
      if (joystickPressed()) { inTetris = false; drawMenu(); }
      return;
    }
    int horizontal = analogRead(JOY_X_PIN);
    int vertical = analogRead(JOY_Y_PIN);
    if (millis() - lastMoveMs > 150) {
      if (horizontal < 350 && fits(pieceX - 1, pieceY, pieceRot)) {
        pieceX--;
        lastMoveMs = millis();
      } else if (horizontal > 700 && fits(pieceX + 1, pieceY, pieceRot)) {
        pieceX++;
        lastMoveMs = millis();
      } else if (vertical < 350) {
        byte nextRotation = (pieceRot + 1) & 3;
        if (fits(pieceX, pieceY, nextRotation)) pieceRot = nextRotation;
        lastMoveMs = millis();
      }
    }
    if (joystickPressed()) {
      while (fits(pieceX, pieceY + 1, pieceRot)) pieceY++;
      lockPiece();
      lastDropMs = millis();
    }
    if (millis() - lastDropMs >= 550) {
      lastDropMs = millis();
      if (fits(pieceX, pieceY + 1, pieceRot)) pieceY++;
      else lockPiece();
    }
    drawTetris();
    return;
  }

  if (inSnake) {
    if (menuButtonPressed()) {
      inSnake = false;
      drawMenu();
      return;
    }
    if (snakeOver) {
      if (joystickPressed()) startSnake();
      return;
    }
    updateSnake();
    return;
  }

  if (inCar) {
    if (menuButtonPressed()) {
      inCar = false;
      drawMenu();
      return;
    }
    if (carOver) {
      if (joystickPressed()) startCar();
      return;
    }
    updateCar();
    return;
  }

  if (inSpace) {
    if (menuButtonPressed()) {
      inSpace = false;
      drawMenu();
      return;
    }
    if (spaceOver) {
      if (joystickPressed()) startSpace();
      return;
    }
    updateSpace();
    return;
  }

  if (inBird) {
    if (menuButtonPressed()) {
      inBird = false;
      drawMenu();
      return;
    }
    if (birdOver) {
      if (joystickPressed()) startBird();
      return;
    }
    updateBird();
    return;
  }

  int vertical = analogRead(JOY_Y_PIN);
  if (millis() - lastMoveMs > 180) {
    if (vertical < 350 && selectedItem > 0) { selectedItem--; lastMoveMs = millis(); drawMenu(); }
    else if (vertical > 700 && selectedItem < 4) { selectedItem++; lastMoveMs = millis(); drawMenu(); }
  }
  if (joystickPressed()) {
    if (selectedItem == 0) startTetris();
    else if (selectedItem == 1) startSnake();
    else if (selectedItem == 2) startBird();
    else if (selectedItem == 3) startCar();
    else startSpace();
  }
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik