Guides · 20 min read

Arduino Games: How to Build Pong, T-Rex, Snake and Simon

Arduino game projects step by step: Pong and T-Rex on an OLED, Snake on an LED matrix, Simon, an LCD runner and a reaction test, plus EEPROM high score code.

Making games with Arduino is the most fun way to learn electronics and programming: with an OLED display, a few buttons and a joystick you can build classics like Pong, the T-Rex runner, Snake and the Simon memory game yourself, then challenge your friends to beat your high score. While writing a game you also pick up the most important programming topics almost by accident: timing, state machines, coordinate systems, collision maths, random numbers and permanent memory. All of these games run on a single Arduino Uno; its 2 KB of RAM is enough as long as you use memory wisely.

In this guide we build six games from start to finish: Pong and the T-Rex runner on an OLED display, Snake on an 8x8 and a 16x16 NeoPixel LED matrix, Simon with four LEDs and four buttons, a runner game on a 16x2 LCD and a reaction test accurate to the millisecond. Every game comes with a parts list, wiring and code that compiles. We first learn the skeleton they all share, meaning the game loop, collision detection, scoring and saving a high score in EEPROM, and then reuse it in every game. At the end you'll also find ESP32 and Raspberry Pi Pico versions.

Arduino Snake game on an 8x8 NeoPixel LED matrix
8x8 WS2812B NeoPixel LED matrix: 64 colour pixels and a single data wire, an ideal mini screen for a Snake game.

Which game needs which parts?

When you make a game with Arduino, the first decision is the display, because the display decides what kind of game you can make. With just a few LEDs you can already build Simon or a reaction test, while a 128×64-pixel OLED makes Pong and T-Rex possible, with moving graphics and physics. The table below sums up the games in this guide from easiest to hardest:

GameDisplayInputOther partsDifficulty
Reaction test1 LED + Serial Monitor1 button220 Ω resistor, optional buzzerEasy
Simon memory game4 coloured LEDs4 buttons4 × 220 Ω resistors, passive buzzerEasy–medium
LCD runner16x2 I2C LCD1 buttonPassive buzzerMedium
Pong0.96" SSD1306 OLEDJoystick or 10 kΩ potentiometerPassive buzzerMedium
T-Rex runner0.96" SSD1306 OLED1 button or the joystick buttonPassive buzzerMedium
Snake (8x8)8x8 NeoPixel matrixJoystick330 Ω resistor, 1000 µF capacitorMedium–advanced
Snake (16x16)16x16 NeoPixel matrixJoystickExternal 5 V 3–4 A power supplyAdvanced
Arduino Uno
Arduino Uno
OLED SSD1306 0.96"
OLED SSD1306 0.96"
Joystick
Joystick
LCD 16x2 I2C
LCD 16x2 I2C
Buzzer
Buzzer
Push button
Push button

Shared wiring: one circuit as a game console

We picked shared pins so that every game except Simon runs on the same circuit. The OLED and the I2C LCD share the same two wires (SDA and SCL); the joystick goes to the analog inputs, the action button to D3 and the buzzer to D8. Once you've built this "game console", you switch games just by uploading different code. You can see your board's full pin map in the Pinout tool.

Part / pinArduino UnoESP32Raspberry Pi Pico
OLED or I2C LCD: SDAA4GPIO 21GP4
OLED or I2C LCD: SCLA5GPIO 22GP5
Joystick VRx (X axis)A0GPIO 34GP26
Joystick VRy (Y axis)A1GPIO 35GP27
Joystick SW (button)D2GPIO 25GP15
Action button (other leg to GND)D3GPIO 26GP14
Passive buzzer (+)D8GPIO 27GP16
NeoPixel DIN (through 330 Ω)D6GPIO 13GP22
On the ESP32 and Pico, power the joystick and the OLED from 3.3 V; a joystick powered from 5 V puts more than 3.3 V on the analog pins and can damage the board. If you connect a 5 V I2C LCD module to an ESP32, add a logic level converter in between. All buttons use the internal pull-up resistor: one leg goes to the pin, the other to GND, and the pin reads LOW when pressed.

How does a game loop work?

Every video game is built on a four-step loop that repeats dozens of times per second: read the input (buttons, joystick), update the world (position, speed, score), check for collisions and draw the screen. One pass through the loop is called a frame. Thirty frames per second (30 FPS) is enough to look smooth, which means each frame lasts about 33 ms. Arduino's loop() function is already a loop, but it runs very fast and irregularly, so you need to split it into fixed frames with millis().

Avoiding delay() here is essential. If you write delay(33), the Arduino does nothing for those 33 ms, can miss a short button press, and the game speed changes whenever drawing takes more or less time. With millis(), loop() reads the input on every pass but only advances the game when it's time for a new frame. You'll find the difference between the two explained in detail in Arduino millis vs delay. The skeleton below is the basis of every game in this guide:

C++ (Arduino IDE)
1// Oyun döngüsü iskeleti / Game loop skeleton
2enum State { MENU, PLAYING, GAME_OVER };   // oyunun durumları / game states
3State state = MENU;
4
5const byte BUTTON_PIN = 3;                 // aksiyon butonu / action button
6const unsigned long FRAME_MS = 33;         // ~30 kare/sn / ~30 frames per second
7unsigned long lastFrame = 0;
8unsigned int score = 0;
9
10// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
11bool buttonPressed() {
12  static bool stable = HIGH, lastRead = HIGH;
13  static unsigned long changedAt = 0;
14  bool r = digitalRead(BUTTON_PIN);
15  if (r != lastRead) { lastRead = r; changedAt = millis(); }
16  if (millis() - changedAt > 15 && r != stable) {   // 15 ms sabit kaldı mı? / stable for 15 ms?
17    stable = r;
18    return stable == LOW;
19  }
20  return false;
21}
22
23void setup() {
24  pinMode(BUTTON_PIN, INPUT_PULLUP);       // basınca LOW / LOW when pressed
25  Serial.begin(9600);
26}
27
28void loop() {
29  bool pressed = buttonPressed();          // 1) giriş: her turda oku / input: read on every pass
30  if (pressed && state != PLAYING) {       // menüden ya da oyun sonundan başlat / start from menu or game over
31    score = 0;
32    state = PLAYING;
33  }
34  if (millis() - lastFrame < FRAME_MS) return;   // kare zamanı gelmedi / not time for a frame yet
35  lastFrame = millis();
36
37  if (state == PLAYING) {
38    score++;                               // 2) güncelle: konum, hız, puan / update: position, speed, score
39    if (score >= 300) state = GAME_OVER;   // 3) çarpışma ya da bitiş kontrolü / collision or end check
40    Serial.println(score);                 // 4) çiz: burada ekrana çizilir / draw: the screen is drawn here
41  }
42}

The second key idea in the code is the state machine. At any moment the game is in one of three states: MENU, PLAYING or GAME_OVER. Each state reacts differently to the same button: in the menu a press starts the game, during play it makes the character jump, and after a game over it starts a new round. Splitting the code like this removes nested if blocks and makes it easy to add a new state, such as PAUSE. We cover the topic in more depth in Arduino state machine.

Reading buttons and a joystick

Wiring buttons with INPUT_PULLUP is the cleanest method: no external resistor is needed, the pin reads HIGH when idle and LOW when pressed. However, a mechanical button bounces for a few milliseconds when it's pressed or released, and the Arduino sees this as several presses in a row. In a game that means jumping twice on one press, or Simon counting a wrong colour. The buttonPressed() function in the skeleton only accepts a new state once the pin has stayed the same for 15 ms, and returns true just once, at the moment of the press. For the circuit and other methods, have a look at How to connect a button to Arduino.

A joystick is made of two potentiometers and a button. On the Uno each axis gives a value from 0 to 1023, and with the stick released the centre is about 512, but this varies from module to module between roughly 490 and 530. That's why grid games use thresholds instead of exact values: below 300 means left or up, above 700 means right or down. The wide zone in between (the dead zone) stops the character from drifting by itself when you let go. In Pong, on the other hand, we map the analog value straight to the paddle: map() converts the 0–1023 range into the paddle's 0–48-pixel travel. Calibration and the direction table are in the joystick module guide.

How does collision detection work?

Collision detection asks whether two objects are in the same place at the same time. In games with a screen, the most common method is the AABB (axis-aligned bounding box) test: you treat each object as a rectangle. Two rectangles overlap only if all four conditions are true together: A's left edge is left of B's right edge, A's right edge is right of B's left edge, and the same two conditions hold on the vertical axis. In code it's a single line: ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by. In grid games (Snake, the LCD runner) it's even simpler: if two cells have the same coordinates, there's a collision.

GameWhat collides with what?Method
PongBall with the walls and paddlesAABB + reversing the velocity
T-RexDinosaur with the cactiAABB (with a slightly smaller box)
SnakeHead with the walls, body and foodGrid cell equality
LCD runnerRunner with a cactusSame column and same row
SimonPressed colour with the colour in the sequenceArray element comparison
Reaction testPress time with signal timeTime comparison (false start)
Small, fast-moving objects can pass straight through an obstacle in a single frame and skip the collision; this is called tunnelling. In Pong, if the ball moves more than 4 pixels in a frame, it crosses the 3-pixel paddle's check zone without ever touching it. The fix is to keep the object's speed per frame smaller than the width of the check zone, and that's exactly what the MAX_SPEED = 2.8 limit in the Pong code does.

Scoring and saving a high score in EEPROM

The score lives in a variable in RAM and disappears the moment you unplug the Arduino. To make a high score permanent you use EEPROM: the Arduino Uno's ATmega328P has 1024 bytes of EEPROM that keeps its contents without power. Two bytes of type uint16_t are enough for a high score (0–65535). EEPROM's only weak spot is write endurance: each cell can be written about 100,000 times. That sounds like a lot, but if you write the score every frame, 30 times per second, the cell wears out in about 55 minutes. The right way is to write only when the game ends and the record has been beaten; even with 10 new records a day the cell will last 27 years.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const int HISCORE_ADDR = 0;          // rekor EEPROM'un 0-1. baytında / high score in EEPROM bytes 0-1
4uint16_t hiScore = 0;
5
6uint16_t loadHiScore() {
7  uint16_t v;
8  EEPROM.get(HISCORE_ADDR, v);       // 2 bayt oku / read 2 bytes
9  if (v == 0xFFFF) v = 0;            // hiç yazılmamış EEPROM 0xFF okunur / blank EEPROM reads 0xFF
10  return v;
11}
12
13void saveIfRecord(uint16_t score) {
14  if (score > hiScore) {             // yalnız rekor kırılınca yaz / write only on a new record
15    hiScore = score;
16    EEPROM.put(HISCORE_ADDR, hiScore);   // put() yalnız değişen baytları yazar / put() writes only changed bytes
17  }
18}
19
20void setup() {
21  Serial.begin(9600);
22  hiScore = loadHiScore();
23  Serial.print(F("Rekor / High score: "));
24  Serial.println(hiScore);
25  saveIfRecord(120);                 // örnek: oyun 120 puanla bitti / example: a game ended with 120 points
26}
27
28void loop() {}

The code has two subtle points. First, every byte of an EEPROM that has never been written is 0xFF, so on the very first start the high score shows up as 65535, and the 0xFFFF check turns it into 0. In the reaction test it's the other way round: 0xFFFF means "no record yet", because a shorter time is better there. Second, EEPROM.put() reads each byte before writing and only writes the bytes that changed, which extends the lifespan even further. For the other EEPROM commands, see Using the Arduino EEPROM. So you can load the games in this guide onto the same Uno one after another, each game has its own address:

GameEEPROM addressStored value
T-Rex runner0–1Highest score
Snake2–3Most food eaten
LCD runner4–5Highest score
Reaction test6–7Shortest time (ms)
Simon8–9Highest level reached

Pong: OLED display and joystick

Pong is the table tennis game released in 1972 that kicked off the video game era. In our version you control the left paddle with the joystick's Y axis (or a potentiometer on A1), and the Arduino plays the right paddle. Parts: an Arduino Uno, a 0.96-inch SSD1306 I2C OLED, a joystick and a passive buzzer. When you install the Adafruit SSD1306 library from the Library Manager, also install its dependency, Adafruit GFX. The display wiring and drawing commands are explained in detail in OLED display SSD1306.

C++ (Arduino IDE)
1#include <Wire.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4
5Adafruit_SSD1306 display(128, 64, &Wire, -1);
6
7const byte JOY_Y_PIN  = A1;          // joystick VRy ya da potansiyometre / joystick VRy or potentiometer
8const byte BUZZER_PIN = 8;           // pasif buzzer / passive buzzer
9const byte PADDLE_H = 16, PADDLE_W = 3;
10const byte WIN_SCORE = 5;            // 5 sayı alan kazanır / first to 5 wins
11const float MAX_SPEED = 2.8;         // raketten kalın olmasın / keep below paddle thickness + ball size
12const unsigned long FRAME_MS = 30;   // ~33 kare/sn / ~33 frames per second
13
14float ballX, ballY, velX, velY;
15float playerY = 24, cpuY = 24;
16byte playerScore = 0, cpuScore = 0;
17unsigned long lastFrame = 0;
18
19void serve(int dir) {                // topu ortadan at / serve from the center
20  ballX = 63; ballY = 31;
21  velX = 1.8 * dir;
22  velY = random(-10, 11) / 10.0;     // -1,0 ile 1,0 arası / between -1.0 and 1.0
23}
24
25// Topun rakete çarptığı nokta yeni açıyı belirler / where the ball hits the paddle sets the new angle
26void hitPaddle(float paddleY) {
27  float offset = (ballY + 1 - (paddleY + PADDLE_H / 2.0)) / (PADDLE_H / 2.0);  // -1 üst, +1 alt / -1 top, +1 bottom
28  velX = constrain(-velX * 1.05, -MAX_SPEED, MAX_SPEED);   // yön değiştir, %5 hızlan / reverse, 5% faster
29  velY = offset * 1.5;
30  tone(BUZZER_PIN, 880, 15);
31}
32
33void setup() {
34  Serial.begin(9600);
35  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
36    Serial.println(F("SSD1306 allocation failed"));
37    for (;;);                        // ekran yoksa dur / halt without a display
38  }
39  display.setTextColor(SSD1306_WHITE);
40  randomSeed(analogRead(A3));        // boştaki analog pin / a floating analog pin
41  serve(1);
42}
43
44void loop() {
45  if (millis() - lastFrame < FRAME_MS) return;
46  lastFrame = millis();
47
48  // 1) Giriş: oyuncu raketi / Input: player paddle
49  playerY = map(analogRead(JOY_Y_PIN), 0, 1023, 0, 64 - PADDLE_H);
50
51  // 2) Basit yapay zekâ: CPU topu sınırlı hızla izler / Simple AI: the CPU follows the ball at limited speed
52  float target = ballY - PADDLE_H / 2.0;
53  cpuY += constrain(target - cpuY, -1.2, 1.2);
54  cpuY = constrain(cpuY, 0, 64 - PADDLE_H);
55
56  // 3) Topu hareket ettir / Move the ball
57  ballX += velX;
58  ballY += velY;
59
60  // 4) Çarpışmalar / Collisions
61  if (ballY <= 0 || ballY >= 62) {   // üst ve alt duvar / top and bottom walls
62    velY = -velY;
63    ballY = constrain(ballY, 0, 62);
64  }
65  if (velX < 0 && ballX <= 4 && ballX > 0 && ballY + 2 >= playerY && ballY <= playerY + PADDLE_H) {
66    ballX = 4;
67    hitPaddle(playerY);
68  }
69  if (velX > 0 && ballX >= 122 && ballX < 126 && ballY + 2 >= cpuY && ballY <= cpuY + PADDLE_H) {
70    ballX = 122;
71    hitPaddle(cpuY);
72  }
73
74  // 5) Sayı / Point
75  if (ballX <= 0)   { cpuScore++;    tone(BUZZER_PIN, 220, 200); serve(1); }
76  if (ballX >= 126) { playerScore++; tone(BUZZER_PIN, 660, 200); serve(-1); }
77
78  // 6) Çiz / Draw
79  display.clearDisplay();
80  for (byte y = 0; y < 64; y += 6) display.drawFastVLine(63, y, 3, SSD1306_WHITE);  // orta çizgi / center line
81  display.fillRect(1, (int)playerY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
82  display.fillRect(124, (int)cpuY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
83  display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
84  display.setTextSize(1);
85  display.setCursor(52, 0); display.print(playerScore);
86  display.setCursor(70, 0); display.print(cpuScore);
87  display.display();
88
89  // 7) Oyun bitti mi? / Game over?
90  if (playerScore >= WIN_SCORE || cpuScore >= WIN_SCORE) {
91    display.setTextSize(2);
92    if (playerScore >= WIN_SCORE) { display.setCursor(22, 24); display.print(F("YOU WIN")); }
93    else                          { display.setCursor(10, 24); display.print(F("GAME OVER")); }
94    display.display();
95    delay(3000);
96    playerScore = cpuScore = 0;
97    serve(1);
98  }
99}

What makes Pong fun is the hitPaddle() function. If the ball hits the middle of the paddle it comes back almost straight; if it hits the edge it bounces off at a sharp angle and puts pressure on the opponent. We do this by scaling the ball's distance from the paddle's centre to a value between -1 and +1 and turning it into vertical speed. The ball speeds up by 5% with every hit and is capped by MAX_SPEED. The Arduino's paddle can move at most 1.2 pixels per frame, so if you give the ball a sharp angle it can't keep up and you score. To make the game harder, raise that value to 1.5; for two players, delete the CPU lines and control the second paddle with a potentiometer on A2.

Why a 30 ms frame time? The Uno sends the 1024-byte frame buffer to the OLED over 400 kHz I2C in about 25 ms, so the display can't show more than 35–40 frames per second. Keeping the frame time just above that transfer time makes the game speed steady. And don't be fooled by the compiler's "Global variables use 28%" report: the SSD1306 buffer is allocated at run time, so real RAM usage climbs to about 78%. If memory runs out you'll see the SSD1306 allocation failed error; we've collected the fixes in Arduino low memory.

T-Rex runner: jump physics and high score

We're bringing the dinosaur game that Chrome shows when you're offline to the OLED display. The dinosaur runs on the spot, cacti scroll from right to left and you jump over them by pressing the button. To make the jump look realistic we use simple physics: pressing the button sets the vertical speed to -4.2 pixels per frame, every frame gravity adds 0.3 to that speed, and the position changes by the speed. So the dinosaur rises while slowing down, hangs at the top for a moment and falls faster and faster; it climbs about 27 pixels and stays in the air for 27 frames, about 0.8 seconds. The parts are the same as for Pong; to jump you use the button on D3 or the joystick button.

C++ (Arduino IDE)
1#include <Wire.h>
2#include <EEPROM.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_SSD1306.h>
5
6Adafruit_SSD1306 display(128, 64, &Wire, -1);
7
8const byte JUMP_PIN     = 3;         // aksiyon butonu / action button
9const byte JOY_SW_PIN   = 2;         // joystick butonu da zıplatır / the joystick button jumps too
10const byte BUZZER_PIN   = 8;
11const int  HISCORE_ADDR = 0;         // EEPROM 0-1. bayt / EEPROM bytes 0-1
12const byte GROUND_Y = 56;            // zemin çizgisi / ground line
13const byte DINO_X = 10, DINO_W = 14, DINO_H = 14;
14const float JUMP_V  = -4.2;          // zıplama hızı (piksel/kare) / jump speed (px/frame)
15const float GRAVITY = 0.3;           // yer çekimi (piksel/kare²) / gravity (px/frame²)
16const unsigned long FRAME_MS = 30;   // ~33 kare/sn / ~33 frames per second
17
18enum State { MENU, PLAYING, GAME_OVER };
19State state = MENU;
20
21struct Cactus { float x; byte w, h; };
22Cactus cacti[2];
23
24float dinoY, velY, speed;
25uint16_t score = 0, hiScore = 0;
26unsigned long lastFrame = 0;
27
28bool jumpHeld() {
29  return digitalRead(JUMP_PIN) == LOW || digitalRead(JOY_SW_PIN) == LOW;
30}
31
32uint16_t loadHiScore() {
33  uint16_t v;
34  EEPROM.get(HISCORE_ADDR, v);
35  return (v == 0xFFFF) ? 0 : v;      // boş EEPROM 0xFFFF okunur / blank EEPROM reads 0xFFFF
36}
37
38void placeCactus(byte i, float startX) {
39  cacti[i].x = startX;
40  cacti[i].w = random(4, 9);         // 4-8 piksel genişlik / 4-8 px wide
41  cacti[i].h = random(8, 17);        // 8-16 piksel yükseklik / 8-16 px tall
42}
43
44void resetGame() {
45  dinoY = GROUND_Y - DINO_H;
46  velY = 0;
47  speed = 2.0;
48  score = 0;
49  placeCactus(0, 140);
50  placeCactus(1, 140 + random(70, 140));
51}
52
53// İki dikdörtgen üst üste biniyor mu? (AABB) / Do two rectangles overlap? (AABB)
54bool overlap(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) {
55  return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
56}
57
58void drawDino(int x, int y, bool legA) {
59  display.fillRect(x + 6, y, 8, 5, SSD1306_WHITE);                  // kafa / head
60  display.drawPixel(x + 9, y + 1, SSD1306_BLACK);                   // göz / eye
61  display.fillRect(x, y + 5, 10, 6, SSD1306_WHITE);                 // gövde / body
62  display.fillRect(x + 2, y + 11, 2, legA ? 3 : 1, SSD1306_WHITE);  // sol bacak / left leg
63  display.fillRect(x + 6, y + 11, 2, legA ? 1 : 3, SSD1306_WHITE);  // sağ bacak / right leg
64}
65
66void drawScene() {
67  display.clearDisplay();
68  display.drawFastHLine(0, GROUND_Y, 128, SSD1306_WHITE);
69  bool onGround = dinoY >= GROUND_Y - DINO_H;
70  drawDino(DINO_X, (int)dinoY, onGround ? (score / 4) % 2 : true);  // koşma animasyonu / running animation
71  for (byte i = 0; i < 2; i++) {
72    display.fillRect((int)cacti[i].x, GROUND_Y - cacti[i].h, cacti[i].w, cacti[i].h, SSD1306_WHITE);
73  }
74  display.setTextSize(1);
75  display.setCursor(0, 0);  display.print(score);
76  display.setCursor(80, 0); display.print(F("HI ")); display.print(hiScore);
77}
78
79void showMessage(const __FlashStringHelper *msg) {
80  display.fillRect(14, 18, 100, 28, SSD1306_BLACK);
81  display.drawRect(14, 18, 100, 28, SSD1306_WHITE);
82  display.setCursor(64 - strlen_P((const char *)msg) * 3, 28);   // ortala (karakter 6 piksel) / center (6 px per char)
83  display.print(msg);
84  display.display();
85}
86
87void gameOver() {
88  tone(BUZZER_PIN, 150, 300);
89  if (score > hiScore) {             // yeni rekor: yalnız şimdi yaz / new record: write only now
90    hiScore = score;
91    EEPROM.put(HISCORE_ADDR, hiScore);
92  }
93  drawScene();
94  showMessage(F("GAME OVER"));
95  delay(800);                        // basılı buton hemen yeniden başlatmasın / a held button must not restart at once
96  state = GAME_OVER;
97}
98
99void setup() {
100  pinMode(JUMP_PIN, INPUT_PULLUP);
101  pinMode(JOY_SW_PIN, INPUT_PULLUP);
102  Serial.begin(9600);
103  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
104    Serial.println(F("SSD1306 allocation failed"));
105    for (;;);
106  }
107  display.setTextColor(SSD1306_WHITE);
108  randomSeed(analogRead(A3));
109  hiScore = loadHiScore();
110  if (digitalRead(JUMP_PIN) == LOW) {    // açılışta basılıysa rekoru sıfırla / held at power-up: reset the record
111    hiScore = 0;
112    EEPROM.put(HISCORE_ADDR, hiScore);
113  }
114  resetGame();
115  drawScene();
116  showMessage(F("PRESS START"));
117}
118
119void loop() {
120  if (millis() - lastFrame < FRAME_MS) return;
121  lastFrame = millis();
122
123  if (state != PLAYING) {            // menü ya da oyun sonu: basınca başla / menu or game over: press to start
124    if (jumpHeld()) {
125      while (jumpHeld());            // bırakılmasını bekle / wait for release
126      resetGame();
127      state = PLAYING;
128    }
129    return;
130  }
131
132  // 1) Giriş ve fizik / Input and physics
133  bool onGround = dinoY >= GROUND_Y - DINO_H;
134  if (onGround && jumpHeld()) {
135    velY = JUMP_V;
136    tone(BUZZER_PIN, 1200, 20);
137  }
138  velY += GRAVITY;                   // her karede hız artar / velocity grows every frame
139  dinoY += velY;
140  if (dinoY > GROUND_Y - DINO_H) { dinoY = GROUND_Y - DINO_H; velY = 0; }   // yere indi / landed
141
142  // 2) Engeller ve çarpışma / Obstacles and collision
143  for (byte i = 0; i < 2; i++) {
144    cacti[i].x -= speed;
145    if (cacti[i].x < -(float)cacti[i].w) {            // ekrandan çıktı / left the screen
146      float otherX = cacti[1 - i].x;
147      placeCactus(i, max(128.0, otherX + random(70, 140)));
148    }
149    // biraz küçük çarpışma kutusu daha adil hissettirir / a slightly smaller hitbox feels fairer
150    if (overlap(DINO_X + 2, (int)dinoY + 1, DINO_W - 4, DINO_H - 2,
151                (int)cacti[i].x, GROUND_Y - cacti[i].h, cacti[i].w, cacti[i].h)) {
152      gameOver();
153      return;
154    }
155  }
156
157  // 3) Puan ve zorluk / Score and difficulty
158  score++;
159  if (score % 200 == 0 && speed < 5.0) speed += 0.25;   // yaklaşık 6 sn'de bir hızlan / speed up every ~6 s
160
161  // 4) Çiz / Draw
162  drawScene();
163  display.display();
164}
SettingValueEffect
JUMP_V-4.2Jump strength; at -5 the dinosaur climbs about 40 pixels, close to the top of the screen
GRAVITY0.3A bigger value makes the jump shorter and sharper
speed2.0 → 5.0Speed of the cacti; grows by 0.25 every 200 frames
random(70, 140)pixelsDistance between two cacti; smaller values make the game harder
Hitbox10×12 pixelsSmaller than the 14×14 drawn dinosaur, so near misses don't count as deaths

The high score is written to EEPROM the moment the game ends, and only if it was beaten; it appears in the top-right corner as HI. If you hold the D3 button while powering up the board, the record is reset, handy when you start a new tournament with friends. The height and width of the cacti come from random(). To make every game start differently, the randomSeed(analogRead(A3)) line uses the noise of an unconnected analog pin as the seed.

Snake on an LED matrix (8x8 and 16x16)

The Snake game made famous by old Nokia phones is best played on an LED matrix, because every LED is one game cell. We use a colour WS2812B NeoPixel matrix: the 8x8 version has 64 pixels and the 16x16 version 256, all driven over a single data wire. The snake's body is stored in two arrays (snakeX and snakeY), and element 0 is the head. On every step the body shifts by one cell and the new position is written to the head; when the snake eats, the tail isn't removed, so it grows by one cell. To compile the code, install the Adafruit NeoPixel library; wiring details are in the WS2812B NeoPixel guide.

C++ (Arduino IDE)
1#include <Adafruit_NeoPixel.h>
2#include <EEPROM.h>
3
4const byte W = 8, H = 8;             // 16x16 matris için 16 yap / set to 16 for a 16x16 matrix
5const bool SERPENTINE = false;       // zikzak dizilimli panelde true / true for zigzag-wired panels
6const byte DATA_PIN = 6;             // DIN, 330 Ω üzerinden / DIN through 330 Ω
7const byte JOY_X = A0, JOY_Y = A1, JOY_SW = 2;
8const byte BUZZER_PIN = 8;
9const int  HISCORE_ADDR = 2;         // EEPROM 2-3. bayt / EEPROM bytes 2-3
10
11Adafruit_NeoPixel matrix(W * H, DATA_PIN, NEO_GRB + NEO_KHZ800);
12
13byte snakeX[W * H], snakeY[W * H];   // [0] baş, sonrası gövde / [0] head, then the body
14int snakeLen;
15int8_t dirX, dirY, nextDX, nextDY;
16byte foodX, foodY;
17unsigned int stepMs;
18unsigned long lastStep = 0;
19uint16_t hiScore;
20bool alive = false;
21
22// (x, y) → LED sırası / (x, y) → LED index
23uint16_t xyToIndex(byte x, byte y) {
24  if (SERPENTINE && (y & 1)) return y * W + (W - 1 - x);   // tek satırlar ters yönde / odd rows run backwards
25  return y * W + x;
26}
27
28bool onSnake(byte x, byte y) {
29  for (int i = 0; i < snakeLen; i++) {
30    if (snakeX[i] == x && snakeY[i] == y) return true;
31  }
32  return false;
33}
34
35void placeFood() {
36  do {                               // yem yılanın üstüne düşmesin / food must not land on the snake
37    foodX = random(W);
38    foodY = random(H);
39  } while (onSnake(foodX, foodY));
40}
41
42void resetGame() {
43  snakeLen = 3;
44  for (int i = 0; i < snakeLen; i++) { snakeX[i] = 3 - i; snakeY[i] = H / 2; }
45  dirX = nextDX = 1;                 // sağa doğru başla / start moving right
46  dirY = nextDY = 0;
47  stepMs = 300;
48  placeFood();
49  alive = true;
50}
51
52void readJoystick() {
53  int x = analogRead(JOY_X), y = analogRead(JOY_Y);
54  // Geri dönüş yasak: kontrol şimdiki yöne göre / no reversing: check against the current direction
55  if      (x < 300 && dirX != 1)  { nextDX = -1; nextDY = 0; }
56  else if (x > 700 && dirX != -1) { nextDX = 1;  nextDY = 0; }
57  else if (y < 300 && dirY != 1)  { nextDX = 0;  nextDY = -1; }
58  else if (y > 700 && dirY != -1) { nextDX = 0;  nextDY = 1; }
59}
60
61void draw() {
62  matrix.clear();
63  matrix.setPixelColor(xyToIndex(foodX, foodY), matrix.Color(255, 0, 0));      // yem kırmızı / food red
64  for (int i = snakeLen - 1; i >= 0; i--) {
65    uint32_t c = (i == 0) ? matrix.Color(255, 200, 0) : matrix.Color(0, 160, 0); // baş sarı / head yellow
66    matrix.setPixelColor(xyToIndex(snakeX[i], snakeY[i]), c);
67  }
68  matrix.show();
69}
70
71void gameOver() {
72  alive = false;
73  uint16_t score = snakeLen - 3;     // yenen yem sayısı / food eaten
74  tone(BUZZER_PIN, 150, 400);
75  if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
76  for (byte k = 0; k < 3; k++) {     // kırmızı yanıp sön / flash red
77    matrix.fill(matrix.Color(80, 0, 0)); matrix.show(); delay(200);
78    matrix.clear();                      matrix.show(); delay(200);
79  }
80  for (uint16_t i = 0; i < score && i < W * H; i++) matrix.setPixelColor(i, matrix.Color(0, 0, 120));  // puan kadar LED / one LED per point
81  matrix.show();
82  Serial.print(F("Skor / Score: "));  Serial.print(score);
83  Serial.print(F("  Rekor / Best: ")); Serial.println(hiScore);
84}
85
86void step() {
87  dirX = nextDX; dirY = nextDY;
88  int nx = snakeX[0] + dirX, ny = snakeY[0] + dirY;
89  if (nx < 0 || nx >= W || ny < 0 || ny >= H) { gameOver(); return; }   // duvar / wall
90
91  bool eat = (nx == foodX && ny == foodY);
92  // Yem yoksa kuyruk ucu boşalacağı için ona çarpmak sayılmaz / without food the tail tip moves away, so it is not a hit
93  int checkLen = eat ? snakeLen : snakeLen - 1;
94  for (int i = 0; i < checkLen; i++) {
95    if (snakeX[i] == nx && snakeY[i] == ny) { gameOver(); return; }    // kendine çarptı / hit itself
96  }
97
98  if (eat) snakeLen++;
99  for (int i = snakeLen - 1; i > 0; i--) {      // gövdeyi bir hücre kaydır / shift the body one cell
100    snakeX[i] = snakeX[i - 1];
101    snakeY[i] = snakeY[i - 1];
102  }
103  snakeX[0] = nx; snakeY[0] = ny;
104
105  if (eat) {
106    tone(BUZZER_PIN, 1000, 40);
107    if (stepMs > 120) stepMs -= 10;             // her yemde hızlan / faster after every food
108    if (snakeLen == W * H) { gameOver(); return; }   // tahta doldu: kazandın / board full: you win
109    placeFood();
110  }
111}
112
113void setup() {
114  pinMode(JOY_SW, INPUT_PULLUP);
115  Serial.begin(9600);
116  matrix.begin();
117  matrix.setBrightness(30);          // düşük parlaklık = düşük akım / low brightness = low current
118  randomSeed(analogRead(A3));
119  EEPROM.get(HISCORE_ADDR, hiScore);
120  if (hiScore == 0xFFFF) hiScore = 0;
121  resetGame();
122  draw();
123}
124
125void loop() {
126  if (!alive) {                      // oyun bitti: joystick butonuyla yeniden / game over: restart with the joystick button
127    if (digitalRead(JOY_SW) == LOW) { resetGame(); draw(); delay(300); }
128    return;
129  }
130  readJoystick();                    // her turda oku, hiçbir hareketi kaçırma / read on every pass, never miss a move
131  if (millis() - lastStep >= stepMs) {
132    lastStep = millis();
133    step();
134    if (alive) draw();
135  }
136}

There are three critical rules in the code. No reversing: if a snake moving right suddenly turns left, it runs into its own neck; readJoystick() checks the new direction against the snake's current direction rather than the pending one, so even two quick moves can't turn it around. Tail exception: on a step without food the tip of the tail moves away, so moving the head into that cell is not a collision. Placing food: placeFood() keeps trying until the food doesn't land on the snake. Each piece of food shortens the step time by 10 ms (from 300 ms down to 120 ms); when the game ends, one LED lights blue for every piece you ate, and the record goes into EEPROM.

It also matters how the LEDs in your matrix are wired. On most rigid 8x8 boards the LEDs run row by row in the same direction (SERPENTINE = false). Flexible 16x16 panels are usually wired in a zigzag: one row runs left to right, the next right to left. If the snake jumps diagonally or runs backwards on every other row, set SERPENTINE to true; if the directions come out reversed, rotate the joystick or swap the 300 and 700 thresholds. For 16x16, just set W and H to 16; the snake arrays then take 512 bytes, the NeoPixel buffer 768 bytes, and about 75% of the Uno's RAM is used.

16x16 NeoPixel LED matrix
16x16 NeoPixel matrix: a 256-pixel playfield means a longer snake and a tougher game; it needs an external 5 V supply.
A single WS2812B draws about 60 mA at full white; the 256 LEDs of a 16x16 matrix can reach 15 A in the worst case, and neither USB nor the Arduino's 5V pin can supply that. Power the matrix from an external 5 V 3–4 A supply, connect the supply's GND to the Arduino's GND, and add a 330 Ω resistor on the data line and a 1000 µF capacitor across the power input. setBrightness(30) in the code cuts brightness to about 12%; since only a few LEDs are lit at once in Snake, the current stays at a few hundred mA. For a single-colour option that uses less RAM, see MAX7219 LED matrix.

Simon memory game: 4 LEDs and 4 buttons

Simon is the legendary memory game released in 1978: each round the device adds one colour to the sequence and you repeat the whole sequence in the same order. Parts: four coloured LEDs (green, red, yellow, blue), a 220 Ω resistor for each, four buttons and a passive buzzer. Connect the LEDs to D9–D12, the buttons to D2–D5 and the buzzer to D8; this game is a separate circuit from the shared console. You can pick the right resistor for your LEDs with the LED resistor calculator. The four sounds of the original Simon are notes of an A major chord; the code uses the same notes (330, 440, 554 and 659 Hz), so it sounds pleasant even when you press quickly.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const byte LED_PINS[4]    = {9, 10, 11, 12};       // yeşil, kırmızı, sarı, mavi / green, red, yellow, blue
4const byte BUTTON_PINS[4] = {2, 3, 4, 5};          // LED'lerle aynı sırada / same order as the LEDs
5const int  TONES[4]       = {330, 440, 554, 659};  // E4, A4, C#5, E5: A majör akoru / A major chord
6const byte BUZZER_PIN     = 8;
7const int  HISCORE_ADDR   = 8;                     // EEPROM 8-9. bayt / EEPROM bytes 8-9
8const byte MAX_LEVEL      = 50;
9
10byte sequence[MAX_LEVEL];
11byte level = 0;
12uint16_t hiScore;
13
14void playColor(byte c, int ms) {
15  digitalWrite(LED_PINS[c], HIGH);
16  tone(BUZZER_PIN, TONES[c]);
17  delay(ms);
18  digitalWrite(LED_PINS[c], LOW);
19  noTone(BUZZER_PIN);
20}
21
22// Basılan rengi döndürür; 3 sn içinde basılmazsa -1 / returns the pressed colour; -1 after 3 s
23int waitForButton() {
24  unsigned long start = millis();
25  while (millis() - start < 3000) {
26    for (byte i = 0; i < 4; i++) {
27      if (digitalRead(BUTTON_PINS[i]) == LOW) {
28        playColor(i, 200);                              // basılan rengi göster / show the pressed colour
29        while (digitalRead(BUTTON_PINS[i]) == LOW);     // bırakılmasını bekle / wait for release
30        delay(30);                                      // sıçramayı atla / skip the bounce
31        return i;
32      }
33    }
34  }
35  return -1;
36}
37
38// LED'ler döner; ilk basış oyunu başlatır ve rastgeleliği tohumlar
39// LEDs chase until the first press, which starts the game and seeds the randomness
40void waitForStart() {
41  byte i = 0;
42  while (true) {
43    digitalWrite(LED_PINS[i], HIGH);
44    for (byte t = 0; t < 20; t++) {                     // 200 ms boyunca butonlara bak / poll the buttons for 200 ms
45      for (byte b = 0; b < 4; b++) {
46        if (digitalRead(BUTTON_PINS[b]) == LOW) {
47          digitalWrite(LED_PINS[i], LOW);
48          randomSeed(micros());                         // basış anı her seferinde farklı / the press time always differs
49          while (digitalRead(BUTTON_PINS[b]) == LOW);
50          delay(600);
51          return;
52        }
53      }
54      delay(10);
55    }
56    digitalWrite(LED_PINS[i], LOW);
57    i = (i + 1) % 4;
58  }
59}
60
61void endGame(uint16_t score) {
62  tone(BUZZER_PIN, 110, 700);
63  for (byte k = 0; k < 3; k++) {                        // tüm LED'ler yanıp söner / all LEDs flash
64    for (byte i = 0; i < 4; i++) digitalWrite(LED_PINS[i], HIGH);
65    delay(200);
66    for (byte i = 0; i < 4; i++) digitalWrite(LED_PINS[i], LOW);
67    delay(200);
68  }
69  if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
70  Serial.print(F("Seviye / Level: "));  Serial.print(score);
71  Serial.print(F("  Rekor / Best: ")); Serial.println(hiScore);
72  level = 0;
73  delay(1000);
74}
75
76void setup() {
77  for (byte i = 0; i < 4; i++) {
78    pinMode(LED_PINS[i], OUTPUT);
79    pinMode(BUTTON_PINS[i], INPUT_PULLUP);              // basınca LOW / LOW when pressed
80  }
81  Serial.begin(9600);
82  EEPROM.get(HISCORE_ADDR, hiScore);
83  if (hiScore == 0xFFFF) hiScore = 0;
84}
85
86void loop() {
87  if (level == 0) waitForStart();
88  if (level == MAX_LEVEL) { endGame(MAX_LEVEL); return; }   // hepsini bildin / you completed every level
89
90  sequence[level++] = random(4);                       // diziye yeni bir renk ekle / append a new colour
91  int showMs = max(180, 520 - level * 20);             // seviye arttıkça hızlanır / faster at higher levels
92  for (byte i = 0; i < level; i++) {                   // diziyi baştan göster / replay the whole sequence
93    playColor(sequence[i], showMs);
94    delay(showMs / 2);
95  }
96  for (byte i = 0; i < level; i++) {                   // oyuncu aynı sırayı tekrarlar / the player repeats it
97    if (waitForButton() != sequence[i]) {              // yanlış ya da geç / wrong or too late
98      endGame(level - 1);
99      return;
100    }
101  }
102  delay(700);                                          // tur arası / pause between rounds
103}

In Simon, randomness is everything: if the sequence is the same in every game, the player just memorises it. The randomSeed(analogRead(A0)) method, which reads an unconnected pin, is weak in practice because the pin often reads similar values. This code uses a more robust trick: the game waits for the player's first press and uses the micros() value at that moment as the seed. Nobody can repeat the moment of a press to the microsecond, so every game starts differently. The display time gets 20 ms shorter with each level and settles at 180 ms; if you don't press within 3 seconds or press the wrong colour, the game ends, and the level you reached is saved to EEPROM if it's a record.

LCD runner game: custom characters on a 16x2 display

You can't draw pixels on a 16x2 character LCD, but with createChar() you can define up to 8 custom characters of 5×8 pixels each. Three are enough for the runner game: two running poses and a cactus. The bottom row of the display is the ground; cacti scroll from right to left, and when you press the button the runner moves to the top row and stays in the air for a few steps. Because this game is grid-based, the collision check is a single line: if there's a cactus in the runner's column while it's on the ground, the game is over. For the library and wiring, see I2C LCD display.

C++ (Arduino IDE)
1#include <Wire.h>
2#include <EEPROM.h>
3#include <LiquidCrystal_I2C.h>
4
5LiquidCrystal_I2C lcd(0x27, 16, 2);  // adres 0x3F de olabilir / the address may also be 0x3F
6
7const byte JUMP_PIN = 3, BUZZER_PIN = 8;
8const int  HISCORE_ADDR = 4;         // EEPROM 4-5. bayt / EEPROM bytes 4-5
9const byte PLAYER_COL = 1;           // koşucunun sütunu / runner's column
10const byte JUMP_STEPS = 4;           // zıplama süresi (adım) / jump length (steps)
11const byte MIN_GAP = 4;              // kaktüsler arası en az boşluk / minimum gap between cacti
12
13// 5x8 özel karakterler / 5x8 custom characters
14byte runA[8]   = {0b01110, 0b01110, 0b00100, 0b01110, 0b10101, 0b00100, 0b01010, 0b10001};
15byte runB[8]   = {0b01110, 0b01110, 0b00100, 0b01110, 0b10101, 0b00100, 0b01010, 0b01010};
16byte cactus[8] = {0b00100, 0b00101, 0b10101, 0b10101, 0b10110, 0b01100, 0b00100, 0b00100};
17
18byte ground[16];                     // 1 = o sütunda kaktüs var / 1 = cactus in that column
19byte gap = 0, jumpLeft = 0;
20uint16_t score = 0, hiScore = 0;
21unsigned int stepMs = 300;
22unsigned long lastStep = 0;
23bool alive = false;
24
25// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
26bool buttonPressed() {
27  static bool stable = HIGH, lastRead = HIGH;
28  static unsigned long changedAt = 0;
29  bool r = digitalRead(JUMP_PIN);
30  if (r != lastRead) { lastRead = r; changedAt = millis(); }
31  if (millis() - changedAt > 15 && r != stable) {
32    stable = r;
33    return stable == LOW;
34  }
35  return false;
36}
37
38void draw() {
39  byte row = jumpLeft > 0 ? 0 : 1;   // havadaysa üst satır / top row while jumping
40  lcd.setCursor(0, 0);
41  for (byte c = 0; c < 11; c++) lcd.write((uint8_t)(row == 0 && c == PLAYER_COL ? 0 : ' '));
42  char buf[6];
43  sprintf(buf, "%5u", score);        // sağa yaslı skor / right-aligned score
44  lcd.print(buf);
45  lcd.setCursor(0, 1);
46  for (byte c = 0; c < 16; c++) {
47    if (row == 1 && c == PLAYER_COL) lcd.write((uint8_t)(score & 1));   // runA / runB sırayla / alternately
48    else lcd.write((uint8_t)(ground[c] ? 2 : ' '));
49  }
50}
51
52void startGame() {
53  memset(ground, 0, sizeof(ground));
54  gap = 0; jumpLeft = 0; score = 0; stepMs = 300;
55  alive = true;
56  lastStep = millis();
57  draw();
58}
59
60void die() {
61  alive = false;
62  tone(BUZZER_PIN, 150, 400);
63  if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
64  lcd.clear();
65  lcd.print(F("GAME OVER ")); lcd.print(score);
66  lcd.setCursor(0, 1);
67  lcd.print(F("BEST ")); lcd.print(hiScore);
68}
69
70void setup() {
71  pinMode(JUMP_PIN, INPUT_PULLUP);
72  lcd.init();
73  lcd.backlight();
74  lcd.createChar(0, runA);
75  lcd.createChar(1, runB);
76  lcd.createChar(2, cactus);
77  EEPROM.get(HISCORE_ADDR, hiScore);
78  if (hiScore == 0xFFFF) hiScore = 0;
79  randomSeed(analogRead(A3));
80  lcd.setCursor(0, 0); lcd.print(F("LCD RUNNER"));
81  lcd.setCursor(0, 1); lcd.print(F("BEST ")); lcd.print(hiScore);
82}
83
84void loop() {
85  if (buttonPressed()) {
86    if (!alive) startGame();                       // başlat / start
87    else if (jumpLeft == 0) {                      // yerdeyse zıpla / jump only from the ground
88      jumpLeft = JUMP_STEPS;
89      tone(BUZZER_PIN, 1200, 20);
90      draw();                                      // zıplamayı hemen göster / show the jump at once
91    }
92  }
93  if (!alive || millis() - lastStep < stepMs) return;
94  lastStep = millis();
95
96  for (byte c = 0; c < 15; c++) ground[c] = ground[c + 1];   // dünya sola kayar / the world scrolls left
97  if (gap >= MIN_GAP && random(100) < 35) { ground[15] = 1; gap = 0; }
98  else { ground[15] = 0; gap++; }
99
100  if (jumpLeft > 0) jumpLeft--;                              // havada geçen bir adım / one step in the air
101  if (jumpLeft == 0 && ground[PLAYER_COL]) { die(); return; } // yerdeyken kaktüse çarptı / hit a cactus on the ground
102
103  score++;
104  if (score % 25 == 0 && stepMs > 120) stepMs -= 15;         // giderek hızlan / speed up gradually
105  draw();
106}

Scrolling the world means copying every element of the ground array one place to the left; the rightmost column gets a new cactus with a 35 percent chance. The MIN_GAP = 4 rule leaves at least four empty columns between two cacti, so every obstacle can be cleared. Every 25 points the step time drops by 15 ms, from 300 ms down to 120 ms. Rewriting both rows on every step instead of wiping the display with lcd.clear() prevents flicker; the clear() command alone takes about 2 ms. If you only see blocks on the screen, turn the contrast trimmer on the back of the module; if you see nothing at all, change the address from 0x27 to 0x3F.

Reaction test: measure your reaction time in milliseconds

This is the simplest game and the most addictive one: an LED lights up at a random moment, you press the button as fast as you can, and the Arduino prints the elapsed time in milliseconds to the Serial Monitor. All you need is an LED on D7 with a 220 Ω resistor, the button on D3 and an optional buzzer. The waiting time is random, between 2 and 5 seconds; if you press while waiting you get a "too early" warning, so you can't cheat by guessing. The time is measured with micros(), your average is calculated and your best time is stored in EEPROM.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const byte LED_PIN = 7, BUTTON_PIN = 3, BUZZER_PIN = 8;
4const int  BEST_ADDR = 6;            // EEPROM 6-7. bayt / EEPROM bytes 6-7
5const bool USE_SOUND = false;        // true: LED ile birlikte bip / true: beep together with the LED
6
7uint16_t best;                       // en iyi süre (ms); 0xFFFF = kayıt yok / best time (ms); 0xFFFF = none
8unsigned long total = 0;
9unsigned int rounds = 0;
10
11bool pressed() { return digitalRead(BUTTON_PIN) == LOW; }
12
13void waitRelease() {
14  while (pressed());
15  delay(50);                         // bırakma sıçramasını atla / skip the release bounce
16}
17
18void setup() {
19  pinMode(LED_PIN, OUTPUT);
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  Serial.begin(9600);
22  EEPROM.get(BEST_ADDR, best);       // boş EEPROM 0xFFFF = kayıt yok / blank EEPROM 0xFFFF = no record
23  Serial.println(F("Baslamak icin butona bas / Press the button to start"));
24}
25
26void loop() {
27  while (!pressed());                // başlatma basışı / start press
28  randomSeed(micros());
29  waitRelease();
30  Serial.println(F("Hazir ol... / Get ready..."));
31
32  unsigned long waitMs = random(2000, 5001);   // 2-5 sn rastgele bekleme / random 2-5 s wait
33  unsigned long start = millis();
34  while (millis() - start < waitMs) {
35    if (pressed()) {                 // erken basış / false start
36      tone(BUZZER_PIN, 150, 500);
37      Serial.println(F("Erken bastin! / Too early!"));
38      waitRelease();
39      return;
40    }
41  }
42
43  unsigned long t0 = micros();
44  digitalWrite(LED_PIN, HIGH);       // işaret / signal
45  if (USE_SOUND) tone(BUZZER_PIN, 2000, 50);
46  while (!pressed() && micros() - t0 < 2000000UL);   // en fazla 2 sn bekle / wait at most 2 s
47  unsigned long ms = (micros() - t0) / 1000;
48  digitalWrite(LED_PIN, LOW);
49
50  if (ms >= 2000) {
51    Serial.println(F("Cok yavas / Too slow"));
52    return;
53  }
54  total += ms; rounds++;
55  Serial.print(F("Sure / Time: ")); Serial.print(ms);
56  Serial.print(F(" ms   Ortalama / Average: ")); Serial.print(total / rounds);
57  Serial.println(F(" ms"));
58  if (ms < best) {                   // daha kısa süre daha iyi / shorter is better
59    best = ms;
60    EEPROM.put(BEST_ADDR, best);
61    tone(BUZZER_PIN, 1500, 150);
62    Serial.println(F("Yeni rekor! / New record!"));
63  }
64  Serial.print(F("Rekor / Best: ")); Serial.print(best); Serial.println(F(" ms"));
65  waitRelease();
66  delay(1000);
67}

The average human reaction time to light is about 200–250 ms; a well-focused player can get a little below that. If you set USE_SOUND to true, a beep plays together with the LED. Reactions to sound are usually a bit faster than to light, so comparing the records of the two modes makes an interesting experiment. Show the time on a 7-segment display or an OLED instead of the Serial Monitor and you have a battery-powered device that works without a computer.

Making games with the ESP32

Most of the code in this guide runs on the ESP32 with small changes: the Adafruit SSD1306 and NeoPixel libraries support the ESP32, and tone() is available in versions 2.x and 3.x of the ESP32 Arduino core. Three things need to change: the pins (the ESP32 column of the shared table), the joystick thresholds and how the high score is saved. The ESP32's ADC is 12-bit and returns values from 0 to 4095, so use about 1200 and 2800 instead of 300 and 700. Connect the joystick to the ADC1 pins, GPIO 32–39, because ADC2 pins can't be read while Wi-Fi is on.

The ESP32 has no real EEPROM; the EEPROM library emulates it in flash and needs EEPROM.begin() and EEPROM.commit() calls. The cleaner way is the Preferences library: it writes values under a namespace and a key name into the NVS partition in flash and spreads the wear by itself. The "tap race" game below counts how many times you can press the button in 5 seconds and stores the record with Preferences:

C++ (Arduino IDE)
1#include <Wire.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4#include <Preferences.h>
5
6Adafruit_SSD1306 display(128, 64, &Wire, -1);   // SDA = GPIO 21, SCL = GPIO 22
7Preferences prefs;
8
9const int BUTTON_PIN = 26;           // aksiyon butonu / action button
10const int BUZZER_PIN = 27;           // pasif buzzer / passive buzzer
11const unsigned long ROUND_MS = 5000; // 5 saniyelik tur / 5-second round
12
13uint32_t best = 0;
14
15// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
16bool buttonPressed() {
17  static bool stable = HIGH, lastRead = HIGH;
18  static unsigned long changedAt = 0;
19  bool r = digitalRead(BUTTON_PIN);
20  if (r != lastRead) { lastRead = r; changedAt = millis(); }
21  if (millis() - changedAt > 10 && r != stable) {
22    stable = r;
23    return stable == LOW;
24  }
25  return false;
26}
27
28void screen(const char *title, uint32_t big, uint32_t small) {
29  display.clearDisplay();
30  display.setTextSize(1);
31  display.setCursor(0, 0);   display.print(title);
32  display.setCursor(0, 56);  display.print("BEST "); display.print(best);
33  display.setTextSize(3);
34  display.setCursor(0, 20);  display.print(big);
35  display.setTextSize(1);
36  display.setCursor(90, 56); display.print(small);
37  display.display();
38}
39
40void setup() {
41  pinMode(BUTTON_PIN, INPUT_PULLUP);
42  Serial.begin(115200);
43  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
44    Serial.println("SSD1306 allocation failed");
45    for (;;) delay(1000);
46  }
47  display.setTextColor(SSD1306_WHITE);
48  prefs.begin("oyun", false);        // "oyun" ad alanı, okuma-yazma / "oyun" namespace, read-write
49  best = prefs.getUInt("rekor", 0);  // kayıt yoksa 0 / 0 when nothing is stored
50}
51
52void loop() {
53  screen("TAP RACE: PRESS", 0, 0);
54  while (!buttonPressed()) delay(1); // başlatma / start
55  for (int i = 3; i > 0; i--) {      // 3-2-1 geri sayım / 3-2-1 countdown
56    screen("GET READY", i, 0);
57    tone(BUZZER_PIN, 800, 80);
58    delay(1000);
59  }
60  tone(BUZZER_PIN, 1600, 150);
61
62  uint32_t taps = 0;
63  unsigned long start = millis(), lastDraw = 0;
64  while (millis() - start < ROUND_MS) {
65    if (buttonPressed()) taps++;
66    if (millis() - lastDraw > 100) { // ekranı 10 kez/sn çiz / redraw 10 times per second
67      lastDraw = millis();
68      screen("TAP!", taps, (ROUND_MS - (millis() - start)) / 1000 + 1);
69    }
70  }
71
72  if (taps > best) {                 // yeni rekor flash'a / new record to flash
73    best = taps;
74    prefs.putUInt("rekor", best);
75    tone(BUZZER_PIN, 2000, 300);
76  }
77  screen("RESULT", taps, 0);
78  Serial.printf("Basis / Taps: %lu  Rekor / Best: %lu\n", (unsigned long)taps, (unsigned long)best);
79  delay(3000);
80}

prefs.getUInt("rekor", 0) returns the second parameter if nothing is stored, so you never have to deal with the EEPROM 0xFFFF problem. Namespace and key names can be at most 15 characters long. Even though the ESP32's processor is many times faster than the Uno's, the OLED uses the same I2C bus, so the frame rate barely changes; for much smoother and colourful games you can use an SPI ILI9341 TFT display. The ESP32's Bluetooth, in turn, is a good starting point for turning a phone into a game controller.

Games with the Raspberry Pi Pico and MicroPython

If you program the Raspberry Pi Pico in MicroPython, the game logic is the same and only the syntax changes: time.ticks_ms() and time.ticks_diff() instead of millis(), read_u16(), which returns 0–65535, instead of analogRead(), and PWM instead of tone(). The Pico has no EEPROM, but thanks to MicroPython's file system it's very easy to write the record to a small text file in flash. The code below is the Pico version of the reaction test and keeps the record in the file rekor.txt; save it to the board as main.py in Thonny and it starts by itself on every power-up. If you're new to MicroPython, read What is the Raspberry Pi Pico? first.

MicroPython
1from machine import Pin, PWM
2import time, random
3
4led = Pin("LED", Pin.OUT)                 # kart üstündeki LED / onboard LED
5button = Pin(14, Pin.IN, Pin.PULL_UP)     # GP14, basınca 0 / 0 when pressed
6buzzer = PWM(Pin(16))                     # GP16, pasif buzzer / passive buzzer
7buzzer.duty_u16(0)                        # başlangıçta sessiz / silent at start
8FILE = "rekor.txt"                        # rekor flash'ta dosyada / record kept in a file on flash
9
10def beep(freq, ms):
11    buzzer.freq(freq)
12    buzzer.duty_u16(32768)                # %50 görev oranı / 50% duty cycle
13    time.sleep_ms(ms)
14    buzzer.duty_u16(0)
15
16def load_best():
17    try:
18        with open(FILE) as f:
19            return int(f.read())
20    except (OSError, ValueError):         # dosya yok ya da bozuk / file missing or corrupt
21        return 99999
22
23def save_best(ms):
24    with open(FILE, "w") as f:
25        f.write(str(ms))
26
27def wait_release():
28    while button.value() == 0:
29        time.sleep_ms(5)
30    time.sleep_ms(50)                     # bırakma sıçraması / release bounce
31
32best = load_best()
33print("Baslamak icin butona bas / Press the button to start")
34
35while True:
36    while button.value() == 1:            # başlatma basışı / start press
37        time.sleep_ms(5)
38    wait_release()
39    print("Hazir ol... / Get ready...")
40
41    wait = random.randint(2000, 5000)     # 2-5 sn rastgele bekleme / random 2-5 s wait
42    start = time.ticks_ms()
43    early = False
44    while time.ticks_diff(time.ticks_ms(), start) < wait:
45        if button.value() == 0:
46            early = True
47            break
48    if early:
49        print("Erken bastin! / Too early!")
50        beep(150, 400)
51        wait_release()
52        continue
53
54    t0 = time.ticks_us()
55    led.on()                              # işaret / signal
56    while button.value() == 1 and time.ticks_diff(time.ticks_us(), t0) < 2000000:
57        pass
58    ms = time.ticks_diff(time.ticks_us(), t0) // 1000
59    led.off()
60
61    if ms >= 2000:
62        print("Cok yavas / Too slow")
63        continue
64    print("Sure / Time:", ms, "ms")
65    if ms < best:                         # daha kısa süre daha iyi / shorter is better
66        best = ms
67        save_best(best)
68        print("Yeni rekor! / New record!")
69        beep(1500, 150)
70    print("Rekor / Best:", best, "ms")
71    wait_release()
72    time.sleep(1)

Using ticks_diff() when measuring time matters: the counters wrap around after a certain value, and simply subtracting two values gives a wrong result at that moment; ticks_diff() handles the wrap-around correctly. For OLED games, the ssd1306 driver you install from Thonny's package manager offers fill_rect(), text() and show() commands similar to Adafruit GFX. The neopixel module you need for a NeoPixel matrix ships with MicroPython. That way you can port Pong and Snake to the Pico with the same logic.

Common mistakes and how to fix them

SymptomCauseFix
Blank screenIf the Serial Monitor shows "SSD1306 allocation failed", RAM has run out; if not, the address or the wiring is wrongPut fixed text inside F() and shrink large arrays; without the message, check the SDA/SCL wires and try address 0x3D
The game starts the same way every timeNo randomSeed() or always the same seedSeed with the micros() value of the player's first press
One press makes it jump twiceButton bounceUse the buttonPressed() function that waits for 15 ms of stability
The high score shows 65535Never-written EEPROM reads 0xFFTreat 0xFFFF as 0
The character drifts when the joystick is releasedThe centre isn't 512 and the dead zone is too narrowKeep the 300/700 thresholds or measure the centre at start-up and calibrate
The snake jumps diagonallyThe matrix is wired in a zigzagSet SERPENTINE = true
The Arduino resets when brightness goes upThe LED matrix draws too much currentLower setBrightness() and use an external 5 V supply
PWM on pin 3 or 11 breaks when tone() playsOn the Uno, tone() uses Timer2Move the part that needs PWM to pin 5, 6, 9 or 10
The game slows down as it goes onSerial.print or unnecessary drawing every frameRemove serial output and keep the frame time above the display's transfer time
The LCD only shows blocksContrast setting or wrong addressTurn the trimmer on the back and try address 0x27 or 0x3F
If a game doesn't behave the way you expect, test the logic without the display first: print the position, speed and state to the Serial Monitor every frame and follow a few frames by hand. Once you've found the bug, delete those lines, because Serial.print costs a few milliseconds per frame and throws off the game speed. If the code won't upload to the board at all, check the Arduino code not uploading guide.

Ideas to take your games further

  • Two-player Pong: delete the CPU lines, control the second paddle with a potentiometer on A2 and run both controllers to opposite ends of the table on long wires.
  • Tilt control: use an MPU6050 instead of the joystick and steer the snake or the paddle by tilting the board.
  • Music and sound effects: play game music with a DFPlayer Mini; for short effects a passive buzzer is enough.
  • Play from your phone: turn your phone into a wireless controller with an HC-05 Bluetooth module.
  • A gamepad for your computer: since the Arduino Leonardo can act as a USB keyboard, you can build a controller for PC games with a joystick and buttons.
  • Scoreboard: show the high scores as scrolling text on a 4-in-1 MAX7219 module.
  • Games of chance: build a roulette wheel with a WS2812B ring or an electronic die with LEDs; the most fun way to practise random().

Making games with Arduino is the most enjoyable way to learn code and hardware at the same time. Once you've understood the game loop, collision detection and the EEPROM high score, designing your own game is just a matter of having an idea; for more inspiration, browse the Arduino project ideas list. In the BlueGrays app you'll find Pong, the OLED T-Rex, 8x8 and 16x16 matrix Snake, Simon, the LCD runner and the reaction test as step-by-step projects with circuit diagrams and ready-to-use code.

Frequently asked questions

What games can you make with an Arduino?

With an Arduino Uno you can make Pong, the T-Rex runner, Flappy Bird-style games, Snake, simple block games, the Simon memory game, a reaction test and games of chance like dice or roulette. A 128×64 OLED is enough for games with graphics, and a NeoPixel LED matrix for colourful grid games. Games like Simon and the reaction test are played with just LEDs and buttons, without any display.

What is the best display for an Arduino game?

For beginners the best choice is the 0.96-inch SSD1306 OLED: it's cheap, connects with just two data wires and draws moving graphics at 128×64 pixels. If you want a larger colour picture, an ILI9341 or ST7789 TFT display fits, and for pixel-art games an 8x8 or 16x16 LED matrix. A 16x2 LCD with custom characters is also enough for simple runner games.

How do you save a high score on Arduino?

If you write the high score to EEPROM, it isn't lost when the power goes off. You write it with EEPROM.put(address, score) and read it with EEPROM.get(address, score); the Arduino Uno has 1024 bytes for this. Since each cell can be written about 100,000 times, only write when the game is over and the record has been beaten.

Can you make a gamepad with an Arduino?

Yes. Boards with the ATmega32U4 processor, such as the Arduino Leonardo, Micro or Pro Micro, can appear to the computer as a USB keyboard, mouse or game controller; they read the joystick and buttons and send key presses. The Arduino Uno can't do this directly, but it can send the joystick values over the serial port to a program on the computer.

Why is my Arduino game slow or laggy?

The most common causes are loops built with delay(), Serial.print calls every frame and the display's transfer time. On the Uno, sending the whole OLED screen takes about 25 ms, so a 30 ms frame time is realistic. Use a fixed frame time with millis() instead of delay(), remove serial output and only draw what's needed.

What is the easiest Arduino game for beginners?

The reaction test is the easiest: it takes one LED, one button and a short sketch, yet it teaches core ideas like timing, random numbers and EEPROM. After that, moving on to Simon with four LEDs and four buttons, and then to Pong and T-Rex on an OLED display, is a good learning path.

See all projects
Paddle Ball Game - SSD1306
#86Intermediate
Premium

Paddle Ball Game - SSD1306

A ball, two paddles and you: you play table tennis, one of the very first video games, on a small OLED screen. Two buttons move your paddle on the right up and down, while the computer controls the le

4.5(47)
65
OLED T-Rex Run: Arduino Dinosaur Game
#87Advanced
Premium

OLED T-Rex Run: Arduino Dinosaur Game

The dinosaur game that pops up when your internet goes down, now on your desk: the dinosaur runs across the small OLED screen, cacti come at you, and you jump with a single button. Your score climbs l

4.7(33)
53
Pixel Snake 8x8 Matrix
#88Advanced
Premium

Pixel Snake 8x8 Matrix

Build the classic snake game with your own hands and play it. On a colorful 8x8 light matrix, you steer a green snake with a joystick and eat red apples; with every apple the snake grows, speeds up a

4.7(39)
57
Matrix Snake 16x16
#89Advanced
Premium

Matrix Snake 16x16

The classic snake game, this time on a huge LED display made of 256 lights. You steer the green snake with a joystick and collect red apples; with every apple the snake grows, your score goes up on a

4.8(52)
67
Arduino Memory Game (4 LEDs + 4 Buttons + Buzzer)
#93Advanced
Premium

Arduino Memory Game (4 LEDs + 4 Buttons + Buzzer)

Do you trust your memory? Four colored LEDs light up in a sequence, each playing its own note, and you repeat the same order by pressing the buttons. Every correct round adds a new color to the sequen

4.8(46)
69
Arduino LCD Runner Game
#98Intermediate
Premium

Arduino LCD Runner Game

Side scrolling endless runner game running on 16x2 character LCD screen. Hero runs automatically, player jumps over obstacles by pressing the button. Random obstacles appear on the top and bottom row. Passive buzzer plays opening melody, jumping sound, 50 point celebration sound and end of game melody. "BlueGrays - Arduino Projects" splash screen is shown on startup. The score is updated instantly in the upper right corner of the screen.

4.6(32)
55
Arduino Reaction Time Test with Sound Cue: Measure Your Reflexes in Milliseconds (LCD + Buzzer + 3 LEDs)
#97Intermediate
Premium

Arduino Reaction Time Test with Sound Cue: Measure Your Reflexes in Milliseconds (LCD + Buzzer + 3 LEDs)

How fast are your reflexes? Press the button: the red LED says get ready, the yellow LED says steady and a random wait of 1.5 to 3.5 seconds begins. The moment the green LED lights up and the buzzer beeps, press again; your reaction time appears on the I2C LCD in milliseconds, with your session best and average below it. Press before green and the screen shows TOO EARLY! with a losing sound. Hold the button for a new player, so everyone races the same wait order.

4.6(33)
50