Community project
Tetris Snake Menu
Kacper Kobrzyński
Published July 16, 2026 · Updated July 16, 2026

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
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| SH1106 OLED128x64 | 1 | 1.3 inch 128x64 OLED display with I2C interface using the SH1106 controller |
| KY-023 Dual Axis Joystick ModuleKY-023 | 1 | Dual-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 button | 1 | Momentary push button switch |
Assembly
4 stepsOdłącz zasilanie
Odłącz Arduino Uno od USB przed zmianą przewodów.
- ⚠ Nie wkładaj ani nie wyjmuj przewodów przy podłączonym USB.
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.
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.
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| Pin | Connection | Type |
|---|---|---|
| 5V | oled VCC | power |
| GND | oled GND | ground |
| GPIO 18 | oled SDA | i2c |
| GPIO 19 | oled SCL | i2c |
| 5V | joystick VCC | power |
| GND | joystick GND | ground |
| GPIO 14 | joystick VRx | adc |
| GPIO 15 | joystick VRy | adc |
| GPIO 2 | joystick SW | digital |
| GND | menu_button GND | ground |
| GPIO 3 | menu_button SIGNAL | digital |
Firmware
Arduino#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.