ESP32 C3 als RETRO-Spielekonsole

Hallo Leute. Schön dass ich hier neu dabei sein darf. Dafür habe ich was für Euch vorbereitet: Habe mal aus Spaß ChatGPT gefragt, ob ich Hilfe bekomme um ein Spiel zu programmieren. Hat tatsächlich geklappt. Space Invaders, Pong und Snake kein Problem. 1982 hat mein Couseng auf nem C64 das Spiel "Mondlandung" selbst geschrieben. Das habe ich jetzt remaked auf nem ESP32 C3 OLED mit einem 0,96 Zoll, 128x64 Display an Pin 8 und 9 (SDA und SCL). Button links an Pin 2, Button rechts an Pin 1, Fire an Pin 10. Absolutes Suchtspiel. Es geht darum, ein Raumschiff auf der Mondoberfläche zu landen. Mit der Feuertaste strotzt man der Mondanziehungskraft. Mit links und rechts navigiert man über das Display. Ziel ist, das Raumschiff auf einer geeigneten Plattform sanft zu landen. Hier der Programmcode:

#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>

// =====================================================
// MONDLANDUNG - Remake für ESP32-C3 + 128x64 OLED von Ralf Genitheim - Original C64 Version von Michael Rausch 1982
// Steuerung:
//   Links  = nach links
//   Rechts = nach rechts
//   Feuer  = Schub nach oben
//
// Annahmen:
// - OLED 128x64 per I2C
// - Taster gegen GND, daher INPUT_PULLUP
// - Taster aktiv LOW
// - 3 Leben
// - unbegrenzter Treibstoff
// - neues Gelände pro Runde
// =====================================================

// ---------- Display ----------
// Für die meisten SSD1306-I2C-OLEDs passend:
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// Falls ihr statt SSD1306 ein SH1106 habt, nimm stattdessen diese Zeile:
// U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// ---------- Pins ----------
// Diese 3 Pins ggf. an euer Retro-Game-Setup anpassen:
const int PIN_LEFT  = 2;
const int PIN_RIGHT = 1;
const int PIN_FIRE  = 10;

// ---------- Spielfeld ----------
const int SCREEN_W = 128;
const int SCREEN_H = 64;

// ---------- Mondgelände ----------
int terrain[SCREEN_W];
int landingPadX = 0;
int landingPadW = 16;

// ---------- Spielzustände ----------
enum GameState {
  STATE_TITLE,
  STATE_PLAYING,
  STATE_SUCCESS,
  STATE_EXPLODING,
  STATE_GAMEOVER
};

GameState gameState = STATE_TITLE;

// ---------- Timing ----------
unsigned long stateStartMs = 0;
unsigned long lastFrameMs = 0;
const unsigned long FRAME_MS = 33; // ~30 FPS

// ---------- Schiff ----------
struct Ship {
  float x;
  float y;
  float vx;
  float vy;
  bool thrustOn;
};

Ship ship;

// ---------- Physik ----------
const float GRAVITY         = 0.045f;
const float THRUST_POWER    = 0.095f;
const float SIDE_ACCEL      = 0.030f;
const float HORIZONTAL_DRAG = 0.985f;
const float MAX_VX          = 0.90f;
const float MAX_VY          = 1.60f;

// ---------- Schiffsform ----------
const int SHIP_BODY_W = 12;
const int SHIP_BODY_H = 7;
const int LEG_SPAN    = 12;
const int LEG_DROP    = 5;
const int FOOT_HALF   = 2;

// ---------- Landekriterien ----------
const float SAFE_LANDING_VY = 0.55f;
const float SAFE_LANDING_VX = 0.45f;

// ---------- Leben ----------
int lives = 3;

// ---------- Explosion ----------
int explosionRadius = 0;

// =====================================================
// Hilfsfunktionen
// =====================================================

bool btnLeft() {
  return digitalRead(PIN_LEFT) == LOW;
}

bool btnRight() {
  return digitalRead(PIN_RIGHT) == LOW;
}

bool btnFire() {
  return digitalRead(PIN_FIRE) == LOW;
}

int clampInt(int v, int lo, int hi) {
  if (v < lo) return lo;
  if (v > hi) return hi;
  return v;
}

float clampFloat(float v, float lo, float hi) {
  if (v < lo) return lo;
  if (v > hi) return hi;
  return v;
}

int shipCenterX() {
  return (int)round(ship.x);
}

int shipCenterY() {
  return (int)round(ship.y);
}

int leftFootX() {
  return shipCenterX() - LEG_SPAN / 2;
}

int rightFootX() {
  return shipCenterX() + LEG_SPAN / 2;
}

int footY() {
  return shipCenterY() + SHIP_BODY_H / 2 + LEG_DROP;
}

void setState(GameState newState) {
  gameState = newState;
  stateStartMs = millis();
}

void generateTerrain() {
  // Grundhöhe des Bodens
  int y = random(42, 54);

  // Zufällige Landefläche
  landingPadW = random(14, 20);
  landingPadX = random(10, SCREEN_W - landingPadW - 10);

  for (int x = 0; x < SCREEN_W; x++) {
    if (x >= landingPadX && x < landingPadX + landingPadW) {
      terrain[x] = y;
    } else {
      int step = random(-2, 3);
      y += step;
      y = clampInt(y, 36, 60);
      terrain[x] = y;
    }
  }

  // Landefläche schön flach machen
  int padY = terrain[landingPadX - 1 < 0 ? landingPadX : landingPadX - 1];
  padY = clampInt(padY, 42, 56);
  for (int x = landingPadX; x < landingPadX + landingPadW; x++) {
    terrain[x] = padY;
  }

  // Links vom Pad leicht angleichen
  for (int x = landingPadX - 1; x >= max(0, landingPadX - 5); x--) {
    if (terrain[x] > terrain[x + 1] + 2) terrain[x] = terrain[x + 1] + 2;
    if (terrain[x] < terrain[x + 1] - 2) terrain[x] = terrain[x + 1] - 2;
  }

  // Rechts vom Pad leicht angleichen
  for (int x = landingPadX + landingPadW; x < min(SCREEN_W, landingPadX + landingPadW + 5); x++) {
    if (terrain[x] > terrain[x - 1] + 2) terrain[x] = terrain[x - 1] + 2;
    if (terrain[x] < terrain[x - 1] - 2) terrain[x] = terrain[x - 1] - 2;
  }
}

void resetShip() {
  ship.x = SCREEN_W / 2.0f;
  ship.y = 10.0f;
  ship.vx = 0.0f;
  ship.vy = 0.12f;
  ship.thrustOn = false;
}

void startNewRound() {
  generateTerrain();
  resetShip();
  explosionRadius = 0;
  setState(STATE_PLAYING);
}

bool isInsideLandingPad(int x) {
  return (x >= landingPadX && x < landingPadX + landingPadW);
}

int terrainYAt(int x) {
  x = clampInt(x, 0, SCREEN_W - 1);
  return terrain[x];
}

void crash() {
  explosionRadius = 2;
  setState(STATE_EXPLODING);
}

void succeed() {
  setState(STATE_SUCCESS);
}

// =====================================================
// Zeichnen
// =====================================================

void drawTerrain() {
  for (int x = 0; x < SCREEN_W - 1; x++) {
    u8g2.drawLine(x, terrain[x], x + 1, terrain[x + 1]);
    u8g2.drawLine(x, terrain[x], x, SCREEN_H - 1);
  }

  // Landefläche hervorheben
  u8g2.drawFrame(landingPadX, terrain[landingPadX] - 2, landingPadW, 3);
}

void drawStars() {
  // feste Pseudo-Sterne für Retro-Look
  for (int i = 0; i < 18; i++) {
    int sx = (i * 37 + 11) % SCREEN_W;
    int sy = (i * 17 + 7) % 26;
    u8g2.drawPixel(sx, sy);
  }
}

void drawShip() {
  int cx = shipCenterX();
  int cy = shipCenterY();

  // Körper: rundlich / kapselartig
  u8g2.drawEllipse(cx, cy, 6, 4, U8G2_DRAW_ALL);

  // kleines Cockpit
  u8g2.drawCircle(cx, cy - 1, 1, U8G2_DRAW_ALL);

  // Beine
  int lx = cx - LEG_SPAN / 2;
  int rx = cx + LEG_SPAN / 2;
  int legTopY = cy + 2;
  int fy = footY();

  u8g2.drawLine(cx - 3, legTopY, lx, fy - 1);
  u8g2.drawLine(cx + 3, legTopY, rx, fy - 1);

  // Füße
  u8g2.drawLine(lx - FOOT_HALF, fy, lx + FOOT_HALF, fy);
  u8g2.drawLine(rx - FOOT_HALF, fy, rx + FOOT_HALF, fy);

  // Abgasstrahl
  if (ship.thrustOn) {
    u8g2.drawLine(cx, fy - 1, cx, fy + 5);
    u8g2.drawLine(cx - 1, fy, cx - 1, fy + 3);
    u8g2.drawLine(cx + 1, fy, cx + 1, fy + 3);
  }
}

void drawHUD() {
  u8g2.setFont(u8g2_font_5x8_tf);

  char buf[24];
  snprintf(buf, sizeof(buf), "LEBEN: %d", lives);
  u8g2.drawStr(0, 7, buf);

  char buf2[24];
  snprintf(buf2, sizeof(buf2), "VY:%d", (int)(ship.vy * 100));
  u8g2.drawStr(82, 7, buf2);
}

void drawExplosion() {
  int cx = shipCenterX();
  int cy = shipCenterY();

  for (int r = 1; r <= explosionRadius; r += 3) {
    u8g2.drawCircle(cx, cy, r, U8G2_DRAW_ALL);
  }

  for (int i = 0; i < 8; i++) {
    float a = i * 3.14159f / 4.0f;
    int x2 = cx + (int)(cos(a) * explosionRadius);
    int y2 = cy + (int)(sin(a) * explosionRadius);
    u8g2.drawLine(cx, cy, x2, y2);
  }
}

void drawTitleScreen() {
  u8g2.clearBuffer();
  drawStars();

  u8g2.setFont(u8g2_font_10x20_tf);
  u8g2.drawStr(10, 22, "MONDLANDUNG");

  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(0, 38, "LINKS / RECHTS / FIRE");
  //u8g2.drawStr(0, 50, "SANFT AUF FELD LANDEN");
  u8g2.drawStr(10, 50, "C64 - ESP32 REMAKE");
  u8g2.drawStr(25, 62, "FIRE ZUM START");

  u8g2.sendBuffer();
}

void drawPlayingScreen() {
  u8g2.clearBuffer();
  drawStars();
  drawTerrain();
  drawShip();
  drawHUD();
  u8g2.sendBuffer();
}

void drawExplosionScreen() {
  u8g2.clearBuffer();
  drawStars();
  drawTerrain();
  drawExplosion();
  drawHUD();

  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(42, 30, "ABSTURZ!");

  u8g2.sendBuffer();
}

void drawSuccessScreen() {
  u8g2.clearBuffer();
  drawStars();
  drawTerrain();
  drawShip();

  u8g2.setFont(u8g2_font_10x20_tf);
  u8g2.drawStr(21, 26, "GELANDET!");

  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(15, 46, "EINE NEUE RUNDE STARTET...");

  u8g2.sendBuffer();
}

void drawGameOverScreen() {
  u8g2.clearBuffer();
  drawStars();

  u8g2.setFont(u8g2_font_10x20_tf);
  u8g2.drawStr(20, 24, "GAME OVER");

  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(5, 42, "Danke MICHAEL Rausch");
  u8g2.drawStr(15, 58, "Fire zum NEUSTART");

  u8g2.sendBuffer();
}

// =====================================================
// Logik
// =====================================================

void updatePlaying() {
  ship.thrustOn = false;

  // Eingaben
  if (btnLeft()) {
    ship.vx -= SIDE_ACCEL;
  }
  if (btnRight()) {
    ship.vx += SIDE_ACCEL;
  }
  if (btnFire()) {
    ship.vy -= THRUST_POWER;
    ship.thrustOn = true;
  }

  // Physik
  ship.vy += GRAVITY;
  ship.vx *= HORIZONTAL_DRAG;

  ship.vx = clampFloat(ship.vx, -MAX_VX, MAX_VX);
  ship.vy = clampFloat(ship.vy, -1.2f, MAX_VY);

  ship.x += ship.vx;
  ship.y += ship.vy;

  // Ränder -> zerstört
  int cx = shipCenterX();
  int cy = shipCenterY();

  if (cx - SHIP_BODY_W / 2 <= 0 || cx + SHIP_BODY_W / 2 >= SCREEN_W - 1 ||
      cy - SHIP_BODY_H / 2 <= 0 || footY() >= SCREEN_H - 1) {
    crash();
    return;
  }

  // Bodenkontakt prüfen an beiden Füßen
  int lfx = leftFootX();
  int rfx = rightFootX();
  int fy  = footY();

  int groundL = terrainYAt(lfx);
  int groundR = terrainYAt(rfx);

  bool leftTouch  = fy >= groundL;
  bool rightTouch = fy >= groundR;

  if (leftTouch || rightTouch) {
    bool onPad = isInsideLandingPad(lfx) && isInsideLandingPad(rfx);
    bool softEnough = (fabs(ship.vy) <= SAFE_LANDING_VY && fabs(ship.vx) <= SAFE_LANDING_VX);
    bool levelEnough = abs(groundL - groundR) <= 1;

    if (onPad && softEnough && levelEnough) {
      // Schiff exakt aufsetzen
      int groundY = terrainYAt((lfx + rfx) / 2);
      ship.y = groundY - LEG_DROP - SHIP_BODY_H / 2;
      ship.vx = 0;
      ship.vy = 0;
      ship.thrustOn = false;
      succeed();
    } else {
      crash();
    }
  }
}

void updateExplosion() {
  explosionRadius += 2;

  if (millis() - stateStartMs > 1000) {
    lives--;
    if (lives <= 0) {
      setState(STATE_GAMEOVER);
    } else {
      startNewRound();
    }
  }
}

void updateSuccess() {
  if (millis() - stateStartMs > 1400) {
    startNewRound();
  }
}

void updateTitle() {
  if (btnFire()) {
    lives = 3;
    startNewRound();
  }
}

void updateGameOver() {
  if (btnFire()) {
    lives = 3;
    setState(STATE_TITLE);
  }
}

// =====================================================
// Setup / Loop
// =====================================================

void setup() {
  pinMode(PIN_LEFT, INPUT_PULLUP);
  pinMode(PIN_RIGHT, INPUT_PULLUP);
  pinMode(PIN_FIRE, INPUT_PULLUP);

  randomSeed(analogRead(A0) + micros());

  u8g2.begin();
  u8g2.setContrast(255);

  setState(STATE_TITLE);
}

void loop() {
  unsigned long now = millis();
  if (now - lastFrameMs < FRAME_MS) return;
  lastFrameMs = now;

  switch (gameState) {
    case STATE_TITLE:
      updateTitle();
      drawTitleScreen();
      break;

    case STATE_PLAYING:
      updatePlaying();
      drawPlayingScreen();
      break;

    case STATE_EXPLODING:
      updateExplosion();
      drawExplosionScreen();
      break;

    case STATE_SUCCESS:
      updateSuccess();
      drawSuccessScreen();
      break;

    case STATE_GAMEOVER:
      updateGameOver();
      drawGameOverScreen();
      break;
  }
}

Schau Dir mal an, wie Code gepostet wird.

Gruß Tommy

An alle Retro-Game-Fans - Hier kann man noch ein wenig Zocken. Den Sketch poste ich nachfolgend. Gleiches Setup wie vorher.

:grinning_face:

/ MENÜ: Links / Rechts auswählen, Fire zum Strarten / links, rechts gleichzeitig bis blaue LED erlischt -> Startmenü


#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C

#define SDA_PIN 8
#define SCL_PIN 9

#define BTN_LEFT  2
#define BTN_RIGHT 1
#define BTN_FIRE  10

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// =====================================================
// Allgemein
// =====================================================

enum Mode {
  MODE_MENU,
  MODE_INVADERS,
  MODE_PONG,
  MODE_SNAKE,
  MODE_BREAKOUT
};

Mode mode = MODE_MENU;
int menuIndex = 0; // 0 Invaders, 1 Pong, 2 Snake, 3 Breakout

unsigned long menuComboStart = 0;
const unsigned long MENU_RETURN_HOLD_MS = 1000;

bool btnPressed(int pin) {
  return digitalRead(pin) == LOW;
}

void clearDisplaySafe() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
}

void waitFireRelease() {
  while (btnPressed(BTN_FIRE)) delay(20);
}

void waitLRRelease() {
  while (btnPressed(BTN_LEFT) || btnPressed(BTN_RIGHT)) delay(20);
}

bool checkReturnToMenuCombo() {
  if (mode == MODE_MENU) {
    menuComboStart = 0;
    return false;
  }

  if (btnPressed(BTN_LEFT) && btnPressed(BTN_RIGHT)) {
    if (menuComboStart == 0) menuComboStart = millis();

    if (millis() - menuComboStart >= MENU_RETURN_HOLD_MS) {
      mode = MODE_MENU;
      menuComboStart = 0;
      waitLRRelease();
      delay(120);
      return true;
    }
  } else {
    menuComboStart = 0;
  }

  return false;
}

// =====================================================
// SPACE INVADERS
// =====================================================

int invPlayerX = 58;
const int invPlayerY = 57;
const int invPlayerW = 12;
const int invPlayerH = 5;

bool invBulletActive = false;
int invBulletX = 0;
int invBulletY = 0;

bool invEnemyBulletActive = false;
int invEnemyBulletX = 0;
int invEnemyBulletY = 0;
unsigned long invLastEnemyShot = 0;
unsigned long invEnemyShotInterval = 900;

int invScore = 0;
bool invGameOver = false;
bool invWinGame = false;
bool invLevelComplete = false;
bool invShowLevelScreen = true;
int invLevel = 1;
int invLives = 3;

unsigned long invLastFireTime = 0;
const unsigned long invFireCooldown = 180;

const int INV_ROWS = 2;
const int INV_COLS = 6;
bool invAlienAlive[INV_ROWS][INV_COLS];
int invAlienBaseX = 10;
int invAlienBaseY = 10;
int invAlienDir = 1;
unsigned long invLastAlienMove = 0;
unsigned long invAlienMoveInterval = 350;
bool invAlienAnimFrame = false;

bool invUfoActive = false;
int invUfoX = -20;
int invUfoY = 3;
int invUfoDir = 1;
unsigned long invLastUfoSpawn = 0;
unsigned long invUfoSpawnDelay = 9000;

void invClearPlayerBullet() { invBulletActive = false; }
void invClearEnemyBullet() { invEnemyBulletActive = false; }

int invAlienX(int r, int c) { return invAlienBaseX + c * 18; }
int invAlienY(int r, int c) { return invAlienBaseY + r * 12; }

int invAliveAliens() {
  int n = 0;
  for (int r = 0; r < INV_ROWS; r++) {
    for (int c = 0; c < INV_COLS; c++) {
      if (invAlienAlive[r][c]) n++;
    }
  }
  return n;
}

void invInitAliensForLevel() {
  for (int r = 0; r < INV_ROWS; r++) {
    for (int c = 0; c < INV_COLS; c++) {
      invAlienAlive[r][c] = true;
    }
  }

  invAlienBaseX = 10;
  invAlienBaseY = 10;
  invAlienDir = 1;
  invAlienAnimFrame = false;
  invClearEnemyBullet();

  if (invLevel == 1) {
    invAlienMoveInterval = 350;
    invEnemyShotInterval = 999999;
  } else if (invLevel == 2) {
    invAlienMoveInterval = 220;
    invEnemyShotInterval = 2200;
  } else {
    invAlienMoveInterval = 180;
    invEnemyShotInterval = 900;
  }
}

void resetInvaders() {
  invScore = 0;
  invLevel = 1;
  invLives = 3;
  invGameOver = false;
  invWinGame = false;
  invLevelComplete = false;
  invShowLevelScreen = true;

  invPlayerX = 58;
  invClearPlayerBullet();
  invClearEnemyBullet();

  invUfoActive = false;
  invUfoX = -20;
  invUfoDir = 1;
  invLastUfoSpawn = millis();

  invInitAliensForLevel();
}

void invSetupNextLevel() {
  invLevel++;

  if (invLevel > 3) {
    invWinGame = true;
    invGameOver = true;
    return;
  }

  invLevelComplete = false;
  invShowLevelScreen = true;
  invPlayerX = 58;
  invClearPlayerBullet();
  invClearEnemyBullet();

  invUfoActive = false;
  invUfoX = -20;
  invLastUfoSpawn = millis();

  invInitAliensForLevel();
}

void invDrawPlayer() {
  display.fillRect(invPlayerX, invPlayerY, invPlayerW, invPlayerH, SSD1306_WHITE);
  display.fillRect(invPlayerX + 4, invPlayerY - 3, 4, 3, SSD1306_WHITE);
}

void invDrawAlien1(int x, int y) {
  display.drawPixel(x + 1, y, SSD1306_WHITE);
  display.drawPixel(x + 6, y, SSD1306_WHITE);
  display.drawLine(x, y + 1, x + 7, y + 1, SSD1306_WHITE);
  display.drawLine(x, y + 2, x + 7, y + 2, SSD1306_WHITE);
  display.drawPixel(x + 2, y + 4, SSD1306_WHITE);
  display.drawPixel(x + 5, y + 4, SSD1306_WHITE);
  display.drawPixel(x, y + 5, SSD1306_WHITE);
  display.drawPixel(x + 7, y + 5, SSD1306_WHITE);
}

void invDrawAlien2(int x, int y) {
  display.drawPixel(x + 2, y, SSD1306_WHITE);
  display.drawPixel(x + 5, y, SSD1306_WHITE);
  display.drawLine(x, y + 1, x + 7, y + 1, SSD1306_WHITE);
  display.drawLine(x, y + 2, x + 7, y + 2, SSD1306_WHITE);
  display.drawPixel(x + 1, y + 4, SSD1306_WHITE);
  display.drawPixel(x + 6, y + 4, SSD1306_WHITE);
  display.drawPixel(x, y + 5, SSD1306_WHITE);
  display.drawPixel(x + 2, y + 5, SSD1306_WHITE);
  display.drawPixel(x + 5, y + 5, SSD1306_WHITE);
  display.drawPixel(x + 7, y + 5, SSD1306_WHITE);
}

void invDrawAliens() {
  for (int r = 0; r < INV_ROWS; r++) {
    for (int c = 0; c < INV_COLS; c++) {
      if (!invAlienAlive[r][c]) continue;
      int x = invAlienX(r, c);
      int y = invAlienY(r, c);
      if (invAlienAnimFrame) invDrawAlien1(x, y);
      else invDrawAlien2(x, y);
    }
  }
}

void invDrawUfo() {
  if (!invUfoActive) return;
  display.drawLine(invUfoX + 2, invUfoY, invUfoX + 9, invUfoY, SSD1306_WHITE);
  display.drawRect(invUfoX, invUfoY + 1, 12, 4, SSD1306_WHITE);
}

void invDrawBullets() {
  if (invBulletActive) {
    display.drawLine(invBulletX, invBulletY, invBulletX, invBulletY - 3, SSD1306_WHITE);
  }
  if (invEnemyBulletActive) {
    display.drawLine(invEnemyBulletX, invEnemyBulletY, invEnemyBulletX, invEnemyBulletY + 3, SSD1306_WHITE);
  }
}

void invDrawHUD() {
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("S:");
  display.print(invScore);

  display.setCursor(40, 0);
  display.print("Lv:");
  display.print(invLevel);

  display.setCursor(78, 0);
  display.print("Li:");
  display.print(invLives);
}

void invDrawGame() {
  clearDisplaySafe();
  invDrawHUD();
  invDrawUfo();
  invDrawAliens();
  invDrawPlayer();
  invDrawBullets();
  display.display();
}

void invDrawEndScreen() {
  clearDisplaySafe();
  display.setTextSize(2);

  if (invWinGame) {
    display.setCursor(18, 18);
    display.print("WIN!");
  } else {
    display.setCursor(10, 10);
    display.print("GAME");
    display.setCursor(10, 34);
    display.print("OVER");
  }

  display.setTextSize(1);
  display.setCursor(22, 56);
  display.print("Score:");
  display.print(invScore);
  display.display();
}

void invDrawLevelScreen() {
  clearDisplaySafe();
  display.setTextSize(2);
  display.setCursor(20, 16);
  display.print("LEVEL ");
  display.print(invLevel);

  display.setTextSize(1);
  display.setCursor(10, 40);
  if (invLevel == 1) display.print("Normal");
  if (invLevel == 2) display.print("Schneller");
  if (invLevel == 3) display.print("Mehr Schuesse");

  display.setCursor(16, 56);
  display.print("Feuer zum Start");
  display.display();
}

void invMovePlayer() {
  if (btnPressed(BTN_LEFT)) invPlayerX -= 2;
  if (btnPressed(BTN_RIGHT)) invPlayerX += 2;

  if (invPlayerX < 0) invPlayerX = 0;
  if (invPlayerX > SCREEN_WIDTH - invPlayerW) invPlayerX = SCREEN_WIDTH - invPlayerW;
}

void invFireBullet() {
  if (!invBulletActive && btnPressed(BTN_FIRE)) {
    if (millis() - invLastFireTime > invFireCooldown) {
      invBulletActive = true;
      invBulletX = invPlayerX + invPlayerW / 2;
      invBulletY = invPlayerY - 1;
      invLastFireTime = millis();
    }
  }
}

void invUpdateUfo() {
  if (!invUfoActive) {
    if (millis() - invLastUfoSpawn > invUfoSpawnDelay) {
      invUfoActive = true;
      invUfoDir = random(0, 2) ? 1 : -1;

      if (invUfoDir == 1) invUfoX = -14;
      else invUfoX = SCREEN_WIDTH + 2;

      invLastUfoSpawn = millis();
      invUfoSpawnDelay = random(7000, 13000);
    }
    return;
  }

  invUfoX += invUfoDir * 2;

  if (invUfoX > SCREEN_WIDTH + 14 || invUfoX < -14) {
    invUfoActive = false;
  }
}

void invUpdatePlayerBullet() {
  if (!invBulletActive) return;

  invBulletY -= 3;

  if (invBulletY < 0) {
    invClearPlayerBullet();
    return;
  }

  if (invUfoActive) {
    if (invBulletX >= invUfoX && invBulletX <= invUfoX + 12 &&
        invBulletY >= invUfoY && invBulletY <= invUfoY + 6) {
      invScore += 50;
      invUfoActive = false;
      invClearPlayerBullet();
      return;
    }
  }

  for (int r = 0; r < INV_ROWS; r++) {
    for (int c = 0; c < INV_COLS; c++) {
      if (!invAlienAlive[r][c]) continue;

      int ax = invAlienX(r, c);
      int ay = invAlienY(r, c);

      if (invBulletX >= ax && invBulletX <= ax + 8 &&
          invBulletY >= ay && invBulletY <= ay + 6) {
        invAlienAlive[r][c] = false;
        invClearPlayerBullet();
        invScore += 10;

        if (invAliveAliens() == 0) invLevelComplete = true;
        return;
      }
    }
  }
}

void invEnemyShoot() {
  if (invEnemyBulletActive) return;
  if (millis() - invLastEnemyShot < invEnemyShotInterval) return;

  int validCols[INV_COLS];
  int validCount = 0;

  for (int c = 0; c < INV_COLS; c++) {
    for (int r = INV_ROWS - 1; r >= 0; r--) {
      if (invAlienAlive[r][c]) {
        validCols[validCount++] = c;
        break;
      }
    }
  }

  if (validCount == 0) return;

  int chosenCol = validCols[random(validCount)];

  for (int r = INV_ROWS - 1; r >= 0; r--) {
    if (invAlienAlive[r][chosenCol]) {
      invEnemyBulletActive = true;
      invEnemyBulletX = invAlienX(r, chosenCol) + 4;
      invEnemyBulletY = invAlienY(r, chosenCol) + 6;
      invLastEnemyShot = millis();
      return;
    }
  }
}

void invUpdateEnemyBullet() {
  if (!invEnemyBulletActive) return;

  invEnemyBulletY += 2;

  if (invEnemyBulletY > SCREEN_HEIGHT) {
    invClearEnemyBullet();
    return;
  }

  if (invEnemyBulletX >= invPlayerX &&
      invEnemyBulletX <= invPlayerX + invPlayerW &&
      invEnemyBulletY >= invPlayerY - 3 &&
      invEnemyBulletY <= invPlayerY + invPlayerH) {
    invClearEnemyBullet();
    invLives--;

    if (invLives <= 0) {
      invGameOver = true;
      invWinGame = false;
    }
  }
}

void invMoveAliens() {
  if (millis() - invLastAlienMove < invAlienMoveInterval) return;

  invLastAlienMove = millis();
  invAlienAnimFrame = !invAlienAnimFrame;

  int minX = 999;
  int maxX = -999;

  for (int r = 0; r < INV_ROWS; r++) {
    for (int c = 0; c < INV_COLS; c++) {
      if (!invAlienAlive[r][c]) continue;
      int x = invAlienX(r, c);
      if (x < minX) minX = x;
      if (x > maxX) maxX = x;
    }
  }

  bool edge = false;
  if (invAlienDir > 0 && maxX + 8 >= SCREEN_WIDTH - 2) edge = true;
  if (invAlienDir < 0 && minX <= 2) edge = true;

  if (edge) {
    invAlienBaseY += 4;
    invAlienDir *= -1;
  } else {
    invAlienBaseX += invAlienDir * 3;
  }

  if (invAlienBaseY + (INV_ROWS - 1) * 12 + 6 >= invPlayerY - 2) {
    invGameOver = true;
    invWinGame = false;
  }
}

void updateInvaders() {
  if (invShowLevelScreen) {
    invDrawLevelScreen();
    if (btnPressed(BTN_FIRE)) {
      waitFireRelease();
      delay(120);
      invShowLevelScreen = false;
    }
    return;
  }

  if (invGameOver) {
    invDrawEndScreen();
    if (btnPressed(BTN_FIRE)) {
      waitFireRelease();
      resetInvaders();
    }
    return;
  }

  if (invLevelComplete) {
    invSetupNextLevel();
    return;
  }

  invMovePlayer();
  invFireBullet();
  invUpdatePlayerBullet();
  invUpdateUfo();
  invEnemyShoot();
  invUpdateEnemyBullet();
  invMoveAliens();
  invDrawGame();
}

// =====================================================
// PONG
// =====================================================

const int pongPaddleW = 3;
const int pongPaddleH = 14;
const int pongLeftPaddleX = 4;
const int pongRightPaddleX = SCREEN_WIDTH - 4 - pongPaddleW;

int pongLeftPaddleY = 25;
int pongRightPaddleY = 25;
const int pongPaddleSpeed = 3;

float pongBallX = SCREEN_WIDTH / 2;
float pongBallY = SCREEN_HEIGHT / 2;
float pongBallVX = 2.0;
float pongBallVY = 1.2;
const int pongBallSize = 3;

int pongScoreLeft = 0;
int pongScoreRight = 0;
const int pongMaxScore = 5;

bool pongGameOver = false;
bool pongWaitingForServe = true;

void resetPongBall(int dir) {
  pongBallX = SCREEN_WIDTH / 2;
  pongBallY = SCREEN_HEIGHT / 2;
  pongBallVX = 2.0 * dir;

  float angles[5] = { -1.6, -0.8, 0.8, 1.2, 1.6 };
  pongBallVY = angles[random(0, 5)];

  pongWaitingForServe = true;
}

void resetPong() {
  pongScoreLeft = 0;
  pongScoreRight = 0;
  pongGameOver = false;

  pongLeftPaddleY = 25;
  pongRightPaddleY = 25;

  resetPongBall(random(0, 2) ? 1 : -1);
}

void pongDrawCenterLine() {
  int centerX = SCREEN_WIDTH / 2;
  for (int y = 0; y < SCREEN_HEIGHT; y += 6) {
    display.drawFastVLine(centerX, y, 3, SSD1306_WHITE);
  }
}

void pongDrawPaddles() {
  display.fillRect(pongLeftPaddleX, pongLeftPaddleY, pongPaddleW, pongPaddleH, SSD1306_WHITE);
  display.fillRect(pongRightPaddleX, pongRightPaddleY, pongPaddleW, pongPaddleH, SSD1306_WHITE);
}

void pongDrawBall() {
  display.fillRect((int)pongBallX, (int)pongBallY, pongBallSize, pongBallSize, SSD1306_WHITE);
}

void pongDrawHUD() {
  display.setTextSize(1);
  display.setCursor(24, 0);
  display.print(pongScoreLeft);
  display.setCursor(98, 0);
  display.print(pongScoreRight);
}

void pongDrawGame() {
  clearDisplaySafe();
  pongDrawCenterLine();
  pongDrawHUD();
  pongDrawPaddles();
  pongDrawBall();
  display.display();
}

void pongDrawServeScreen() {
  pongDrawGame();
  display.fillRect(30, 26, 68, 12, SSD1306_BLACK);
  display.drawRect(30, 26, 68, 12, SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(41, 29);
  display.print("FEUER");
  display.display();
}

void pongDrawGameOver() {
  clearDisplaySafe();
  display.setTextSize(2);
  display.setCursor(26, 8);
  if (pongScoreLeft > pongScoreRight) display.print("WIN");
  else display.print("LOSE");

  display.setTextSize(1);
  display.setCursor(34, 34);
  display.print(pongScoreLeft);
  display.print(" : ");
  display.print(pongScoreRight);

  display.setCursor(14, 54);
  display.print("Feuer = Neustart");
  display.display();
}

void pongMoveLeftPaddle() {
  if (btnPressed(BTN_LEFT)) pongLeftPaddleY -= pongPaddleSpeed;
  if (btnPressed(BTN_RIGHT)) pongLeftPaddleY += pongPaddleSpeed;

  if (pongLeftPaddleY < 0) pongLeftPaddleY = 0;
  if (pongLeftPaddleY > SCREEN_HEIGHT - pongPaddleH) pongLeftPaddleY = SCREEN_HEIGHT - pongPaddleH;
}

void pongMoveRightPaddleAI() {
  int paddleCenter = pongRightPaddleY + pongPaddleH / 2;
  int ballCenter = (int)pongBallY + pongBallSize / 2;

  if (ballCenter < paddleCenter - 2) pongRightPaddleY -= 2;
  if (ballCenter > paddleCenter + 2) pongRightPaddleY += 2;

  if (pongRightPaddleY < 0) pongRightPaddleY = 0;
  if (pongRightPaddleY > SCREEN_HEIGHT - pongPaddleH) pongRightPaddleY = SCREEN_HEIGHT - pongPaddleH;
}

void pongLaunchBallIfReady() {
  if (pongWaitingForServe && btnPressed(BTN_FIRE)) {
    waitFireRelease();
    pongWaitingForServe = false;
    delay(80);
  }
}

void pongUpdateBall() {
  if (pongWaitingForServe) return;

  pongBallX += pongBallVX;
  pongBallY += pongBallVY;

  if (pongBallY <= 0) {
    pongBallY = 0;
    pongBallVY = -pongBallVY;
  }

  if (pongBallY >= SCREEN_HEIGHT - pongBallSize) {
    pongBallY = SCREEN_HEIGHT - pongBallSize;
    pongBallVY = -pongBallVY;
  }

  if (pongBallX <= pongLeftPaddleX + pongPaddleW &&
      pongBallX + pongBallSize >= pongLeftPaddleX &&
      pongBallY + pongBallSize >= pongLeftPaddleY &&
      pongBallY <= pongLeftPaddleY + pongPaddleH) {

    pongBallX = pongLeftPaddleX + pongPaddleW + 1;
    pongBallVX = abs(pongBallVX) * 1.05;

    float hitPos = ((pongBallY + pongBallSize / 2.0) - pongLeftPaddleY) / pongPaddleH;
    pongBallVY = (hitPos - 0.5) * 4.0;

    if (pongBallVX > 5) pongBallVX = 5;
  }

  if (pongBallX + pongBallSize >= pongRightPaddleX &&
      pongBallX <= pongRightPaddleX + pongPaddleW &&
      pongBallY + pongBallSize >= pongRightPaddleY &&
      pongBallY <= pongRightPaddleY + pongPaddleH) {

    pongBallX = pongRightPaddleX - pongBallSize - 1;
    pongBallVX = -abs(pongBallVX) * 1.05;

    float hitPos = ((pongBallY + pongBallSize / 2.0) - pongRightPaddleY) / pongPaddleH;
    pongBallVY = (hitPos - 0.5) * 4.0;

    if (pongBallVX < -5) pongBallVX = -5;
  }

  if (pongBallX < -pongBallSize) {
    pongScoreRight++;
    if (pongScoreRight >= pongMaxScore) pongGameOver = true;
    else resetPongBall(-1);
  }

  if (pongBallX > SCREEN_WIDTH) {
    pongScoreLeft++;
    if (pongScoreLeft >= pongMaxScore) pongGameOver = true;
    else resetPongBall(1);
  }
}

void updatePong() {
  if (pongGameOver) {
    pongDrawGameOver();
    if (btnPressed(BTN_FIRE)) {
      waitFireRelease();
      resetPong();
    }
    return;
  }

  pongMoveLeftPaddle();
  pongMoveRightPaddleAI();
  pongLaunchBallIfReady();
  pongUpdateBall();

  if (pongWaitingForServe) pongDrawServeScreen();
  else pongDrawGame();
}

// =====================================================
// SNAKE
// =====================================================

const uint8_t SNK_CELL = 4;
const uint8_t SNK_COLS = SCREEN_WIDTH / SNK_CELL;
const uint8_t SNK_ROWS = SCREEN_HEIGHT / SNK_CELL;
const uint16_t SNK_MAX_LEN = SNK_COLS * SNK_ROWS;

uint8_t snkX[SNK_MAX_LEN];
uint8_t snkY[SNK_MAX_LEN];
uint16_t snkLen = 0;

uint8_t snkFoodX = 0;
uint8_t snkFoodY = 0;

int8_t snkDirX = 1;
int8_t snkDirY = 0;

bool snkGameOver = false;
bool snkPaused = false;
uint16_t snkScore = 0;

unsigned long snkLastMove = 0;
unsigned long snkMoveInterval = 180;
const unsigned long snkMinInterval = 70;

bool snkLeftLatch = false;
bool snkRightLatch = false;
bool snkFireLatch = false;

void snkPlaceFood() {
  bool ok = false;

  while (!ok) {
    ok = true;
    snkFoodX = random(0, SNK_COLS);
    snkFoodY = random(0, SNK_ROWS);

    for (uint16_t i = 0; i < snkLen; i++) {
      if (snkX[i] == snkFoodX && snkY[i] == snkFoodY) {
        ok = false;
        break;
      }
    }
  }
}

void resetSnake() {
  snkLen = 4;
  snkScore = 0;
  snkGameOver = false;
  snkPaused = false;
  snkMoveInterval = 180;
  snkLastMove = millis();

  uint8_t startX = 8;
  uint8_t startY = 8;

  for (uint16_t i = 0; i < snkLen; i++) {
    snkX[i] = startX - i;
    snkY[i] = startY;
  }

  snkDirX = 1;
  snkDirY = 0;

  snkLeftLatch = false;
  snkRightLatch = false;
  snkFireLatch = false;

  snkPlaceFood();
}

void snkTurnLeft() {
  int8_t ndx = snkDirY;
  int8_t ndy = -snkDirX;
  snkDirX = ndx;
  snkDirY = ndy;
}

void snkTurnRight() {
  int8_t ndx = -snkDirY;
  int8_t ndy = snkDirX;
  snkDirX = ndx;
  snkDirY = ndy;
}

void snkHandleInput() {
  bool leftNow = btnPressed(BTN_LEFT);
  bool rightNow = btnPressed(BTN_RIGHT);
  bool fireNow = btnPressed(BTN_FIRE);

  if (leftNow && !snkLeftLatch && !rightNow && !snkGameOver && !snkPaused) {
    snkTurnLeft();
  }

  if (rightNow && !snkRightLatch && !leftNow && !snkGameOver && !snkPaused) {
    snkTurnRight();
  }

  if (fireNow && !snkFireLatch) {
    if (snkGameOver) {
      resetSnake();
    } else {
      snkPaused = !snkPaused;
    }
  }

  snkLeftLatch = leftNow;
  snkRightLatch = rightNow;
  snkFireLatch = fireNow;
}

void snkMove() {
  if (snkPaused || snkGameOver) return;
  if (millis() - snkLastMove < snkMoveInterval) return;

  snkLastMove = millis();

  int newX = snkX[0] + snkDirX;
  int newY = snkY[0] + snkDirY;

  if (newX < 0 || newX >= SNK_COLS || newY < 0 || newY >= SNK_ROWS) {
    snkGameOver = true;
    return;
  }

  bool eat = (newX == snkFoodX && newY == snkFoodY);

  uint16_t checkLen = eat ? snkLen : (snkLen - 1);
  for (uint16_t i = 0; i < checkLen; i++) {
    if (snkX[i] == newX && snkY[i] == newY) {
      snkGameOver = true;
      return;
    }
  }

  uint16_t newLen = snkLen + (eat ? 1 : 0);
  if (newLen > SNK_MAX_LEN) newLen = SNK_MAX_LEN;

  for (int i = newLen - 1; i > 0; i--) {
    snkX[i] = snkX[i - 1];
    snkY[i] = snkY[i - 1];
  }

  snkX[0] = newX;
  snkY[0] = newY;
  snkLen = newLen;

  if (eat) {
    snkScore++;
    if (snkMoveInterval > snkMinInterval) {
      snkMoveInterval -= 4;
      if (snkMoveInterval < snkMinInterval) snkMoveInterval = snkMinInterval;
    }

    if (snkLen < SNK_MAX_LEN) {
      snkPlaceFood();
    }
  }
}

void snkDrawHUD() {
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("S:");
  display.print(snkScore);

  display.setCursor(40, 0);
  display.print("L:");
  display.print(snkLen);

  display.setCursor(84, 0);
  display.print("V:");
  display.print(260 - snkMoveInterval);
}

void snkDrawBoard() {
  display.fillRect(snkFoodX * SNK_CELL, snkFoodY * SNK_CELL, SNK_CELL, SNK_CELL, SSD1306_WHITE);

  for (uint16_t i = 0; i < snkLen; i++) {
    if (i == 0) {
      display.drawRect(snkX[i] * SNK_CELL, snkY[i] * SNK_CELL, SNK_CELL, SNK_CELL, SSD1306_WHITE);
      display.fillRect(snkX[i] * SNK_CELL + 1, snkY[i] * SNK_CELL + 1, SNK_CELL - 2, SNK_CELL - 2, SSD1306_WHITE);
    } else {
      display.fillRect(snkX[i] * SNK_CELL, snkY[i] * SNK_CELL, SNK_CELL, SNK_CELL, SSD1306_WHITE);
    }
  }
}

void snkDrawPause() {
  display.fillRect(34, 26, 60, 12, SSD1306_BLACK);
  display.drawRect(34, 26, 60, 12, SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(48, 29);
  display.print("PAUSE");
}

void snkDrawGameOver() {
  display.fillRect(20, 18, 88, 28, SSD1306_BLACK);
  display.drawRect(20, 18, 88, 28, SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(35, 23);
  display.print("GAME OVER");
  display.setCursor(28, 35);
  display.print("Feuer = Neu");
}

void snkDrawGame() {
  clearDisplaySafe();
  snkDrawBoard();
  snkDrawHUD();

  if (snkPaused) snkDrawPause();
  if (snkGameOver) snkDrawGameOver();

  display.display();
}

void updateSnake() {
  bool leftNow = btnPressed(BTN_LEFT);
  bool rightNow = btnPressed(BTN_RIGHT);
  bool fireNow = btnPressed(BTN_FIRE);

  if (leftNow && !snkLeftLatch && !rightNow && !snkGameOver && !snkPaused) {
    snkTurnLeft();
  }

  if (rightNow && !snkRightLatch && !leftNow && !snkGameOver && !snkPaused) {
    snkTurnRight();
  }

  if (fireNow && !snkFireLatch) {
    if (snkGameOver) resetSnake();
    else snkPaused = !snkPaused;
  }

  snkLeftLatch = leftNow;
  snkRightLatch = rightNow;
  snkFireLatch = fireNow;

  snkMove();
  snkDrawGame();
}

// =====================================================
// BREAKOUT
// =====================================================

const int brkPaddleW = 20;
const int brkPaddleH = 3;
const int brkPaddleY = 60;
int brkPaddleX = (SCREEN_WIDTH - brkPaddleW) / 2;
const int brkPaddleSpeed = 3;

float brkBallX = 64;
float brkBallY = 48;
float brkBallVX = 1.8;
float brkBallVY = -1.8;
const int brkBallSize = 3;

const int BRK_ROWS = 4;
const int BRK_COLS = 8;
const int brkBrickW = 14;
const int brkBrickH = 5;
const int brkBrickGap = 2;
const int brkBrickStartX = 3;
const int brkBrickStartY = 10;
bool brkBrickAlive[BRK_ROWS][BRK_COLS];

int brkScore = 0;
int brkLives = 3;
int brkLevel = 1;
bool brkGameOver = false;
bool brkWinGame = false;
bool brkWaitingServe = true;
bool brkShowLevelScreen = true;

void brkInitBricks() {
  for (int r = 0; r < BRK_ROWS; r++) {
    for (int c = 0; c < BRK_COLS; c++) {
      brkBrickAlive[r][c] = true;
    }
  }
}

void brkResetBallPaddle() {
  brkPaddleX = (SCREEN_WIDTH - brkPaddleW) / 2;
  brkBallX = brkPaddleX + brkPaddleW / 2 - 1;
  brkBallY = brkPaddleY - 4;

  float speed = 1.8 + (brkLevel - 1) * 0.25;
  brkBallVX = speed;
  brkBallVY = -speed;

  brkWaitingServe = true;
}

void resetBreakout() {
  brkScore = 0;
  brkLives = 3;
  brkLevel = 1;
  brkGameOver = false;
  brkWinGame = false;
  brkShowLevelScreen = true;

  brkInitBricks();
  brkResetBallPaddle();
}

void brkNextLevel() {
  brkLevel++;
  if (brkLevel > 5) {
    brkWinGame = true;
    brkGameOver = true;
    return;
  }

  brkShowLevelScreen = true;
  brkInitBricks();
  brkResetBallPaddle();
}

int brkAliveBricks() {
  int n = 0;
  for (int r = 0; r < BRK_ROWS; r++) {
    for (int c = 0; c < BRK_COLS; c++) {
      if (brkBrickAlive[r][c]) n++;
    }
  }
  return n;
}

void brkDrawHUD() {
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("S:");
  display.print(brkScore);

  display.setCursor(42, 0);
  display.print("L:");
  display.print(brkLevel);

  display.setCursor(84, 0);
  display.print("Li:");
  display.print(brkLives);
}

void brkDrawBricks() {
  for (int r = 0; r < BRK_ROWS; r++) {
    for (int c = 0; c < BRK_COLS; c++) {
      if (!brkBrickAlive[r][c]) continue;

      int x = brkBrickStartX + c * (brkBrickW + brkBrickGap);
      int y = brkBrickStartY + r * (brkBrickH + brkBrickGap);
      display.fillRect(x, y, brkBrickW, brkBrickH, SSD1306_WHITE);
    }
  }
}

void brkDrawPaddle() {
  display.fillRect(brkPaddleX, brkPaddleY, brkPaddleW, brkPaddleH, SSD1306_WHITE);
}

void brkDrawBall() {
  display.fillRect((int)brkBallX, (int)brkBallY, brkBallSize, brkBallSize, SSD1306_WHITE);
}

void brkDrawGame() {
  clearDisplaySafe();
  brkDrawHUD();
  brkDrawBricks();
  brkDrawPaddle();
  brkDrawBall();
  display.display();
}

void brkDrawServeScreen() {
  brkDrawGame();
  display.fillRect(28, 28, 72, 12, SSD1306_BLACK);
  display.drawRect(28, 28, 72, 12, SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(39, 31);
  display.print("FEUER");
  display.display();
}

void brkDrawLevelScreen() {
  clearDisplaySafe();
  display.setTextSize(2);
  display.setCursor(20, 16);
  display.print("LEVEL ");
  display.print(brkLevel);
  display.setTextSize(1);
  display.setCursor(16, 56);
  display.print("Feuer zum Start");
  display.display();
}

void brkDrawEndScreen() {
  clearDisplaySafe();
  display.setTextSize(2);

  if (brkWinGame) {
    display.setCursor(18, 18);
    display.print("WIN!");
  } else {
    display.setCursor(10, 10);
    display.print("GAME");
    display.setCursor(10, 34);
    display.print("OVER");
  }

  display.setTextSize(1);
  display.setCursor(22, 56);
  display.print("Score:");
  display.print(brkScore);
  display.display();
}

void brkMovePaddle() {
  if (btnPressed(BTN_LEFT)) brkPaddleX -= brkPaddleSpeed;
  if (btnPressed(BTN_RIGHT)) brkPaddleX += brkPaddleSpeed;

  if (brkPaddleX < 0) brkPaddleX = 0;
  if (brkPaddleX > SCREEN_WIDTH - brkPaddleW) brkPaddleX = SCREEN_WIDTH - brkPaddleW;

  if (brkWaitingServe) {
    brkBallX = brkPaddleX + brkPaddleW / 2 - 1;
  }
}

void brkLaunchBall() {
  if (brkWaitingServe && btnPressed(BTN_FIRE)) {
    waitFireRelease();
    brkWaitingServe = false;
  }
}

void brkUpdateBall() {
  if (brkWaitingServe) return;

  brkBallX += brkBallVX;
  brkBallY += brkBallVY;

  if (brkBallX <= 0) {
    brkBallX = 0;
    brkBallVX = -brkBallVX;
  }

  if (brkBallX >= SCREEN_WIDTH - brkBallSize) {
    brkBallX = SCREEN_WIDTH - brkBallSize;
    brkBallVX = -brkBallVX;
  }

  if (brkBallY <= 8) {
    brkBallY = 8;
    brkBallVY = -brkBallVY;
  }

  // Paddle
  if (brkBallY + brkBallSize >= brkPaddleY &&
      brkBallY <= brkPaddleY + brkPaddleH &&
      brkBallX + brkBallSize >= brkPaddleX &&
      brkBallX <= brkPaddleX + brkPaddleW &&
      brkBallVY > 0) {

    brkBallY = brkPaddleY - brkBallSize - 1;
    brkBallVY = -abs(brkBallVY);

    float hitPos = ((brkBallX + brkBallSize / 2.0) - brkPaddleX) / brkPaddleW;
    brkBallVX = (hitPos - 0.5) * 4.2;

    if (brkBallVX > -0.6 && brkBallVX < 0.6) {
      brkBallVX = (brkBallVX < 0) ? -0.6 : 0.6;
    }
  }

  // Bricks
  for (int r = 0; r < BRK_ROWS; r++) {
    for (int c = 0; c < BRK_COLS; c++) {
      if (!brkBrickAlive[r][c]) continue;

      int bx = brkBrickStartX + c * (brkBrickW + brkBrickGap);
      int by = brkBrickStartY + r * (brkBrickH + brkBrickGap);

      if (brkBallX + brkBallSize >= bx &&
          brkBallX <= bx + brkBrickW &&
          brkBallY + brkBallSize >= by &&
          brkBallY <= by + brkBrickH) {

        brkBrickAlive[r][c] = false;
        brkScore += 10;
        brkBallVY = -brkBallVY;

        if (brkAliveBricks() == 0) {
          brkNextLevel();
        }
        return;
      }
    }
  }

  // Ball verloren
  if (brkBallY > SCREEN_HEIGHT) {
    brkLives--;
    if (brkLives <= 0) {
      brkGameOver = true;
      brkWinGame = false;
    } else {
      brkResetBallPaddle();
    }
  }
}

void updateBreakout() {
  if (brkShowLevelScreen) {
    brkDrawLevelScreen();
    if (btnPressed(BTN_FIRE)) {
      waitFireRelease();
      delay(120);
      brkShowLevelScreen = false;
    }
    return;
  }

  if (brkGameOver) {
    brkDrawEndScreen();
    if (btnPressed(BTN_FIRE)) {
      waitFireRelease();
      resetBreakout();
    }
    return;
  }

  brkMovePaddle();
  brkLaunchBall();
  brkUpdateBall();

  if (brkWaitingServe) brkDrawServeScreen();
  else brkDrawGame();
}

// =====================================================
// MENÜ
// =====================================================

void drawMenu() {
  clearDisplaySafe();

  display.setTextSize(2);
  display.setCursor(25, 0);
  display.print("SPIELE");

  display.setTextSize(1);

  display.setCursor(8, 17);
  if (menuIndex == 0) display.print(">");
  display.setCursor(20, 17);
  display.print("SPACE INVADERS");

  display.setCursor(8, 30);
  if (menuIndex == 1) display.print(">");
  display.setCursor(20, 30);
  display.print("PONG REMAKE");

  display.setCursor(8, 43);
  if (menuIndex == 2) display.print(">");
  display.setCursor(20, 43);
  display.print("SNAKE ATTACK");

  display.setCursor(8, 56);
  if (menuIndex == 3) display.print(">");
  display.setCursor(20, 56);
  display.print("BREAKOUT LIGHT");

  //display.setCursor(2, 58);
  //display.print("F=Start  L/R=Waehlen");

  display.display();
}

void updateMenu() {
  drawMenu();

  if (btnPressed(BTN_LEFT)) {
    menuIndex--;
    if (menuIndex < 0) menuIndex = 3;
    waitLRRelease();
    delay(80);
    return;
  }

  if (btnPressed(BTN_RIGHT)) {
    menuIndex++;
    if (menuIndex > 3) menuIndex = 0;
    waitLRRelease();
    delay(80);
    return;
  }

  if (btnPressed(BTN_FIRE)) {
    waitFireRelease();
    delay(100);

    if (menuIndex == 0) {
      resetInvaders();
      mode = MODE_INVADERS;
    } else if (menuIndex == 1) {
      resetPong();
      mode = MODE_PONG;
    } else if (menuIndex == 2) {
      resetSnake();
      mode = MODE_SNAKE;
    } else {
      resetBreakout();
      mode = MODE_BREAKOUT;
    }
  }
}

// =====================================================
// SETUP / LOOP
// =====================================================

void setup() {
  pinMode(BTN_LEFT, INPUT_PULLUP);
  pinMode(BTN_RIGHT, INPUT_PULLUP);
  pinMode(BTN_FIRE, INPUT_PULLUP);

  Wire.begin(SDA_PIN, SCL_PIN);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    while (true) delay(100);
  }

  clearDisplaySafe();
  display.display();

  randomSeed(micros());

  clearDisplaySafe();
  display.setTextSize(2);
  display.setCursor(30, 0);
  display.print("ESP32");
  display.display();
  delay(900);

  clearDisplaySafe();
  display.setTextSize(2);
  display.setCursor(25, 0);
  //display.setCursor(25, 22);
  display.print("ARCADE");
  display.setCursor(30, 22);
  display.print("GAMES");
  display.display();
  delay(900);

  display.setTextSize(1);
  display.setCursor(10, 50);
  display.print("by RALF GENITHEIM");
  display.display();
  delay(2700);

  mode = MODE_MENU;
}

void loop() {
  if (checkReturnToMenuCombo()) {
    return;
  }

  if (mode == MODE_MENU) {
    updateMenu();
    delay(20);
    return;
  }

  if (mode == MODE_INVADERS) {
    updateInvaders();
    delay(20);
    return;
  }

  if (mode == MODE_PONG) {
    updatePong();
    delay(20);
    return;
  }

  if (mode == MODE_SNAKE) {
    updateSnake();
    delay(20);
    return;
  }

  if (mode == MODE_BREAKOUT) {
    updateBreakout();
    delay(20);
    return;
  }
}