PAC-MAN

Typ_projektu
Arduino
Zdjecie główne
Krótki opis projektu

Projekt to gra Pac-man z dodatkowym modułem Maszyny do Manualnej Inicjacji Procesów Probabilistycznych (MMIPP), stworzona na mikrokontrolerze Arduino UNO. System operuje na zaawansowanej pętli sterowania, która w czasie rzeczywistym analizuje sygnały z przycisków, zarządza fizyką obiektów oraz precyzyjnie weryfikuje kolizje między graczem a przeciwnikami.

Fundamentem rozgrywki jest globalny licznik monet, stanowiący wspólną walutę dla obu modułów. Aby zoptymalizować wykorzystanie pamięci RAM, dane labiryntów oraz klatki animacji bitmapowych zostały zapisane w pamięci Flash. Dzięki zastosowaniu algorytmów losowości, ruchy duchów w labiryncie są nieprzewidywalne, co znacząco podnosi poziom wyzwania, a wyniki losowań nagród są zawsze unikalne.

Niezbędne elementy

1. Płytka Arduino UNO

2. Płytka SIC

3. Wyświetlacz

4. 6 przycisków

5. Obudowa

Opis projektu

ZASADY I MECHANIKA ROZGRYWKI
- System ekonomiczny: Gracz buduje kapitał, kończąc etapy zręcznościowe. Zgromadzone monety można spróbować pomnożyć w Maszynie do Manualnej Inicjacji Procesów Probabilistycznych lub wydać w sklepie na unikalne, animowane dodatki.

- Moduł Pac-man: Celem jest zebranie wszystkich punktów w labiryncie przy jednoczesnym unikaniu kontaktu z duchami. Gra oferuje trzy poziomy trudności, które zmieniają geometrię mapy, liczbę przeciwników oraz tempo rozgrywki.

- Maszyna Losująca (MMIPP): Pozwala na losowanie symboli graficznych w celu zdobycia wysokich premii punktowych. Nagroda zależy od układu znaków – mała nagroda (20p) przysługuje za trafienie dwóch takich samych znaków, duża wygrana (80p) trzech takich samych znaków, a najwyższa wygrana (JACKPOT - 1000p) za trafienie trzech siódemek.

- Zintegrowany Sklep: Umożliwia zakup i odtwarzanie interaktywnych emotek. Każda z nich posiada własną, dwuklatkową animację, która ożywa na ekranie po dokonaniu transakcji.

INSTRUKCJA OBSŁUGI I STEROWANIE
Urządzenie wykorzystuje intuicyjny układ sześciu przycisków sterujących:

- Strzałki: Odpowiadają za poruszanie się w labiryncie, nawigację w menu oraz wybór towarów w sklepie.

- Przycisk 4: Służy do startu gry, zatwierdzania wybranych opcji oraz uruchamiania mechanizmu losującego MMIPP.

- Przycisk 5: Umożliwia błyskawiczne wywołanie menu sklepu lub powrót do głównego interfejsu.

Przebieg gry:
1) Inicjacja: Po uruchomieniu wybierz poziom trudności strzałkami i potwierdź wybór Przyciskiem 4.

2) Etap zręcznościowy: Zbieraj punkty w labiryncie, unikając duchów. Bezpośredni kontakt z przeciwnikiem przerywa sesję gry.

3) Zmiana trybu: Aby przełączyć konsolę w tryb maszyny losującej, należy wykonać szybką sekwencję przycisków: Góra, a następnie Dół.

4) Losowanie nagród: W module losującym MMIPP naciśnij Przycisk 4, aby postawić 20 monet i wprawić bębny w ruch. Wynik zobaczysz po zakończeniu animacji.

5) Personalizacja: Wejdź do sklepu Przyciskiem 5, zakup wybraną emotkę i uruchom jej animację ponownym kliknięciem przycisku zatwierdzenia.

Zdjęcia
kod programu
// ==========================================
// DOŁĄCZENIE BIBLIOTEK I KONFIGURACJA EKRANU
// ==========================================
// Biblioteki odpowiedzialne za komunikację sprzętową i obsługę wyświetlacza OLED
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// Definicje parametrów ekranu OLED (adres I2C, rozdzielczość)
#define i2c_Address 0x3c 
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1   
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ==========================================
// ZMIENNE WSPÓLNE / STEROWANIE
// ==========================================
// Przypisanie pinów mikrokontrolera do przycisków fizycznych
#define BTN_SHOP  5  
#define BTN_UP    7
#define BTN_DOWN  9
#define BTN_LEFT  10
#define BTN_RIGHT 8
#define BTN_ACCEPT 4
#define BUTTON_PIN 4 

// Zmienne śledzące aktualny stan urządzenia (wybrana gra, czy jesteśmy w sklepie)
int currentGame = 0; 
bool inShop = false; 

// Zmienne do śledzenia stanów przycisków (debounce i detekcja zmiany stanu)
bool lastShopState = HIGH;
bool lastUpState = HIGH;
bool lastDownState = HIGH;
unsigned long lastUpPressTime = 0; 

// ==========================================
// GLOBALNY SYSTEM MONETARNY
// ==========================================
// Główna waluta współdzielona między grami i sklepem
int globalCoins = 100; 

// ==========================================
// ZMIENNE I FUNKCJE SKLEPU
// ==========================================
// Stan asortymentu w sklepie
int currentShopItem = 0; 
const int totalShopItems = 3;  
bool ownsEmote[] = {false, false, false};  

// Funkcja zwracająca cenę w zależności od wybranego przedmiotu
int getShopPrice(int item) {
  if (item == 0) return 50;
  if (item == 1) return 60;
  return 80;
}

// --- TABLICE BITOWE EMOTEK (32x32) W PROGMEM ---
// Zapisane w pamięci Flash mikrokontrolera (PROGMEM) aby oszczędzać pamięć RAM

// 1. EMOTKA HEHE (2 klatki animacji)
const unsigned char PROGMEM emote_frame_1[] = {
  0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xf0, 
  0x1f, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xfe, 
  0x7f, 0x1f, 0xf8, 0xfe, 0xff, 0x1f, 0xf8, 0xff, 0xff, 0x1f, 0xf8, 0xff, 0xff, 0x1f, 0xf8, 0xff, 
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
  0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0x7f, 0xe0, 0x07, 0xfe, 0x7f, 0xf0, 0x0f, 0xfe, 
  0x7f, 0xf8, 0x1f, 0xfe, 0x3f, 0xfc, 0x3f, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xf8, 
  0x0f, 0xff, 0xff, 0xf0, 0x07, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00
};
const unsigned char PROGMEM emote_frame_2[] = {
  0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xf0, 
  0x1f, 0x0f, 0xf0, 0xf8, 0x3f, 0x0f, 0xf0, 0xfc, 0x3f, 0x0f, 0xf0, 0xfc, 0x7f, 0xff, 0xff, 0xfe, 
  0x7f, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 
  0xff, 0x80, 0x01, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0x80, 0x01, 0xff, 
  0xff, 0x80, 0x01, 0xff, 0xff, 0xc0, 0x03, 0xff, 0x7f, 0xe0, 0x07, 0xfe, 0x7f, 0xf0, 0x0f, 0xfe, 
  0x7f, 0xf8, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xf8, 
  0x0f, 0xff, 0xff, 0xf0, 0x07, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00
};

// 2. EMOTKA PLACZ (2 klatki animacji)
const unsigned char PROGMEM emote2_frame_1[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xe1, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 
  0x01, 0x80, 0x01, 0x80, 0x03, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x00, 0x30, 
  0x18, 0x00, 0x00, 0x18, 0x10, 0x40, 0x02, 0x08, 0x30, 0x30, 0x0c, 0x0c, 0x20, 0x08, 0x10, 0x04, 
  0x20, 0x30, 0x0c, 0x04, 0x60, 0x30, 0x0c, 0x06, 0x60, 0x00, 0x00, 0x06, 0x60, 0x30, 0x0c, 0x06, 
  0x60, 0x30, 0x0c, 0x06, 0x60, 0x30, 0x0c, 0x06, 0x60, 0x30, 0x0c, 0x06, 0x20, 0x30, 0x0c, 0x04, 
  0x20, 0x00, 0x00, 0x04, 0x30, 0x07, 0xe0, 0x0c, 0x10, 0x08, 0x10, 0x08, 0x18, 0x00, 0x00, 0x18, 
  0x0c, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xc0, 0x01, 0x80, 0x01, 0x80, 
  0x00, 0xe0, 0x07, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00
};
const unsigned char PROGMEM emote2_frame_2[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xe1, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 
  0x01, 0x80, 0x01, 0x80, 0x03, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x00, 0x30, 
  0x18, 0x00, 0x00, 0x18, 0x10, 0x40, 0x02, 0x08, 0x30, 0x30, 0x0c, 0x0c, 0x20, 0x08, 0x10, 0x04, 
  0x20, 0x30, 0x0c, 0x04, 0x60, 0x30, 0x0c, 0x06, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 
  0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0x60, 0x30, 0x0c, 0x06, 0x20, 0x30, 0x0c, 0x04, 
  0x20, 0x30, 0x0c, 0x04, 0x30, 0x37, 0xec, 0x0c, 0x10, 0x38, 0x1c, 0x08, 0x18, 0x37, 0xec, 0x18, 
  0x0c, 0x37, 0xec, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xc0, 0x01, 0x80, 0x01, 0x80, 
  0x00, 0xe0, 0x07, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00
};

// 3. EMOTKA DUSZEK (2 klatki animacji)
const unsigned char PROGMEM emote3_frame_1[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x3f, 0xfc, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 
  0x03, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 
  0x07, 0xcf, 0xf3, 0xe0, 0x07, 0xcf, 0xf3, 0xe0, 0x07, 0xcf, 0xf3, 0xe0, 0x07, 0xff, 0xff, 0xe0, 
  0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xfc, 0x3f, 0xe0, 0x07, 0xfc, 0x3f, 0xe0, 
  0x07, 0xfc, 0x3f, 0xe0, 0x07, 0xfc, 0x3f, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 
  0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xdb, 0xdb, 0xe0, 0x06, 0xdb, 0xdb, 0x60, 
  0x04, 0x92, 0x49, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const unsigned char PROGMEM emote3_frame_2[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x3f, 0xfc, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 
  0x03, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 
  0x07, 0xff, 0xff, 0xe0, 0x07, 0x87, 0xe1, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 
  0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xf8, 0x1f, 0xe0, 
  0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 
  0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xdb, 0xdb, 0xe0, 0x06, 0xdb, 0xdb, 0x60, 
  0x04, 0x92, 0x49, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// Funkcja odpowiedzialna za wyświetlanie wybranej emotki (naprzemienne wyświetlanie 2 klatek)
void playEmoteAnimation(int emoteIndex) {
  const unsigned char* frame1;
  const unsigned char* frame2;

  // Wybór odpowiedniej pary klatek na podstawie indeksu emotki
  if (emoteIndex == 0) {
    frame1 = emote_frame_1; frame2 = emote_frame_2;
  } else if (emoteIndex == 1) {
    frame1 = emote2_frame_1; frame2 = emote2_frame_2;
  } else {
    frame1 = emote3_frame_1; frame2 = emote3_frame_2;
  }

  // Pętla odtwarzająca animację (4 cykle przełączenia klatek z opóźnieniem)
  for(int i=0; i<4; i++) {
    display.clearDisplay();
    display.drawBitmap(48, 16, frame1, 32, 32, SH110X_WHITE);
    display.display();
    delay(200);

    display.clearDisplay();
    display.drawBitmap(48, 16, frame2, 32, 32, SH110X_WHITE);
    display.display();
    delay(200);
  }
}

// ==========================================
// ZMIENNE I DEFINICJE DLA GRY: PAC-MAN
// ==========================================
// Wzory plansz (labiryntów) dla poszczególnych poziomów trudności (1 - ściana, 2 - kropka)
const uint8_t PROGMEM initial_maze_easy[7][16] = {
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,2,2,2,2,1,2,2,2,2,1,2,2,2,2,1},
  {1,2,1,1,2,1,2,1,1,2,1,2,1,1,2,1},
  {1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1},
  {1,2,1,1,2,1,1,1,1,1,1,2,1,1,2,1},
  {1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
};

const uint8_t PROGMEM initial_maze_hard[7][16] = {
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,2,2,2,1,2,2,2,2,2,2,1,2,2,2,1},
  {1,2,1,2,1,2,1,1,1,1,2,1,2,1,2,1},
  {1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1},
  {1,2,1,2,1,1,1,2,2,1,1,1,2,1,2,1},
  {1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
};

const uint8_t PROGMEM initial_maze_impossible[7][16] = {
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,2,2,2,1,2,2,2,2,2,2,1,2,2,2,1},
  {1,2,1,2,2,2,1,1,1,1,2,2,2,1,2,1},
  {1,2,1,1,1,2,2,2,2,2,2,1,1,1,2,1},
  {1,2,2,2,1,2,1,1,1,1,2,1,2,2,2,1},
  {1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
};

// Aktualnie modyfikowalna plansza w pamięci RAM
uint8_t maze[7][16]; 

// Stan logiczny gry Pac-Man (pozycja gracza, kierunek, punkty)
int pac_r = 1, pac_c = 1; 
int pac_dir = 0, next_dir = 0;
int score = 0, gameDelay = 150; 
int ghost_r[3], ghost_c[3], ghost_dir[3]; 
int activeGhosts = 1; 
bool inMenu = true, gameOver = false, gameWon = false, mouthOpen = false;
int selectedLevel = 0, dotsLeft = 0;      

// Funkcja resetująca stan gry Pac-Man i wczytująca odpowiedni labirynt do pamięci
void startGame() {
  score = 0; pac_r = 1; pac_c = 1;
  pac_dir = 0; next_dir = 0;
  gameOver = false; gameWon = false; dotsLeft = 0;

  // Początkowe ustawienie duchów na planszy
  ghost_r[0] = 5; ghost_c[0] = 14; ghost_dir[0] = 2; 
  ghost_r[1] = 1; ghost_c[1] = 14; ghost_dir[1] = 2;
  ghost_r[2] = 1; ghost_c[2] = 8;  ghost_dir[2] = 1; 

  // Pętla odczytująca strukturę labiryntu z PROGMEM w zależności od poziomu trudności
  for(int r=0; r<7; r++){
    for(int c=0; c<16; c++){
      if (selectedLevel == 0) {
        maze[r][c] = pgm_read_byte(&(initial_maze_easy[r][c]));
        gameDelay = 150; activeGhosts = 1; 
      } else if (selectedLevel == 1) {
        maze[r][c] = pgm_read_byte(&(initial_maze_hard[r][c]));
        gameDelay = 100; activeGhosts = 2; 
      } else { 
        maze[r][c] = pgm_read_byte(&(initial_maze_impossible[r][c]));
        gameDelay = 70; activeGhosts = 3; 
      }
      // Zliczanie kropek na mapie (wymaganych do wygranej)
      if (maze[r][c] == 2) dotsLeft++;
    }
  }
}

// ==========================================
// ZMIENNE I FUNKCJE DLA GRY: JEDNORĘKI BANDYTA
// ==========================================

// Funkcja zwracająca znak do wyświetlenia na podstawie wylosowanej liczby
char getSymbol(int r) {
  if (r == 0) return '7';
  if (r == 1) return '$';
  if (r == 2) return '@';
  if (r == 3) return 'X';
  return 'O';
}

// Funkcja odpowiedzialna za renderowanie aktualnej ilości monet na pasku stanu
void drawCoins() {
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  
  // Dynamiczne przeliczanie długości stringa, by prawidłowo wyrównać tekst do prawej krawędzi
  int temp = globalCoins;
  int digits = 1;
  if (temp >= 10) digits = 2;
  if (temp >= 100) digits = 3;
  if (temp >= 1000) digits = 4;
  if (temp >= 10000) digits = 5;
  
  int strLen = 7 + digits; 
  int pxWidth = strLen * 6;
  
  display.setCursor(128 - pxWidth, 0);
  display.print(F("MONETY:"));
  display.print(globalCoins);
}

// Funkcja wyświetlająca ekran powitalny automatu
void showSlotWelcome() {
  display.clearDisplay();
  
  drawCoins(); 
  
  display.setTextSize(1); 
  display.setTextColor(SH110X_WHITE);
  
  // Zmienione wyśrodkowanie dłuższego tekstu na 2 linie
  display.setCursor(25, 15);
  display.println(F("SPRAWDZ SWOJE")); 
  display.setCursor(37, 25);
  display.println(F("SZCZESCIE")); 
  
  display.setCursor(31, 40);
  display.println(F("Nacisnij 4!"));
  
  display.display();
}

// Funkcja rysująca pojedynczy "bęben" maszyny ze znaczkiem w środku
void drawReel(int x, int y, char symbol) {
  display.drawRect(x, y, 32, 34, SH110X_WHITE);
  display.setTextSize(3); 
  display.setCursor(x + 8, y + 6);
  display.print(symbol);
}

// ==========================================
// GLOWNY SETUP WSPÓLNY DLA CAŁOŚCI
// ==========================================
void setup() {
  // Inicjalizacja pinów dla wszystkich przycisków
  pinMode(BTN_SHOP, INPUT_PULLUP);
  pinMode(BTN_UP, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);
  pinMode(BTN_LEFT, INPUT_PULLUP);
  pinMode(BTN_RIGHT, INPUT_PULLUP);
  pinMode(BTN_ACCEPT, INPUT_PULLUP); 

  // Inicjalizacja generatora liczb pseudolosowych na podst. szumu z wolnego pinu analogowego
  randomSeed(analogRead(0));
  delay(250); 

  // Rozpoczęcie komunikacji z wyświetlaczem OLED (Zatrzymanie programu w razie błędu)
  if (!display.begin(i2c_Address, true)) {
    pinMode(13, OUTPUT);
    while(true) {
      digitalWrite(13, HIGH);
      delay(100);
      digitalWrite(13, LOW);
      delay(100);
    }
  }
  
  // Obrót ekranu, aby pasował do obudowy/ustawienia
  display.setRotation(2);
}

// ==========================================
// GLOWNA PETLA - OBSŁUGA WYBORU GIER I SKLEPU
// ==========================================
void loop() {
  bool currentShopState = digitalRead(BTN_SHOP);
  bool currentUpState = digitalRead(BTN_UP);
  bool currentDownState = digitalRead(BTN_DOWN);
  unsigned long currentMillis = millis();

  // --- 1. WEJŚCIE I WYJŚCIE ZE SKLEPU (Przycisk 5) ---
  // Blok obsługujący otwieranie interfejsu sklepu za pomocą specjalnego przycisku
  if (lastShopState == HIGH && currentShopState == LOW) {
    bool canOpenShop = true;
    
    // Zapobiegamy otwarciu sklepu w trakcie trwania rozgrywki Pac-Mana
    if (currentGame == 0 && !inMenu && !gameOver && !gameWon) {
      canOpenShop = false; 
    }

    if (canOpenShop) {
      inShop = !inShop; 
      
      if (!inShop && currentGame == 1) {
        showSlotWelcome(); 
      }
    }
    
    // POPRAWKA: Czekamy na puszczenie przycisku i debouncing
    while(digitalRead(BTN_SHOP) == LOW) {
      delay(10);
    }
    delay(50); 
  }
  lastShopState = currentShopState;

  // --- 2. PRZEŁĄCZANIE GRY (GÓRA, a następnie DÓŁ) ---
  // Rejestracja sekwencji klawiszy zmieniających aktywną grę (Pac-Man <-> Slot)
  if (lastUpState == HIGH && currentUpState == LOW) {
    lastUpPressTime = currentMillis;
  }
  lastUpState = currentUpState;

  if (lastDownState == HIGH && currentDownState == LOW) {
    if (lastUpPressTime > 0 && (currentMillis - lastUpPressTime < 500)) {
      
      bool canSwitchGame = true;
      if (currentGame == 0 && !inMenu && !gameOver && !gameWon) {
        canSwitchGame = false;
      }

      // Aktualizacja stanu aktywnej aplikacji
      if (canSwitchGame) {
        currentGame = 1 - currentGame;
        inShop = false; 
        display.clearDisplay();
        
        if (currentGame == 1) {
          showSlotWelcome();
        } else {
          inMenu = true;
          gameOver = false;
          gameWon = false;
        }
        delay(250); 
      }
      lastUpPressTime = 0; 
    }
  }
  lastDownState = currentDownState;

  // --- 3. ROZDZIELNIK STANÓW ---
  // W zależności od przypisanych wartości (inShop / currentGame) odpalamy konkretną pętlę podprogramu
  if (inShop) {
    loopShop();
  } else if (currentGame == 0) {
    loopPacman();
  } else {
    loopSlotMachine();
  }
}

// ==========================================
// PETLA SKLEPU
// ==========================================
void loopShop() {
  display.clearDisplay();
  drawCoins();

  // Renderowanie nagłówka i interfejsu wyboru (karuzeli przedmiotów)
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  
  display.setCursor(48, 8);
  display.print(F("SKLEP"));

  if (currentShopItem == 0) {
    display.setCursor(19, 22);
    display.print(F("< EMOTKA HEHE >"));
  } else if (currentShopItem == 1) {
    display.setCursor(16, 22);
    display.print(F("< EMOTKA PLACZ >"));
  } else {
    display.setCursor(13, 22);
    display.print(F("< EMOTKA DUSZEK >"));
  }

  int currentPrice = getShopPrice(currentShopItem);

  // Blok logiczny sprawdzający status przedmiotu - renderuje opcje cenowe lub stan posiadania
  if (!ownsEmote[currentShopItem]) {
    display.setCursor(38, 38);
    display.print(F("CENA: "));
    display.print(currentPrice);
  } else {
    display.setCursor(42, 38);
    display.print(F("KUPIONE"));
  }

  // Wskazówki przycisków
  display.setCursor(10, 56);
  if (!ownsEmote[currentShopItem]) {
    display.print(F("4-KUP  5-WYJDZ < >"));
  } else {
    display.print(F("4-UZYJ 5-WYJDZ < >"));
  }

  display.display();

  // Obsługa przewijania karuzeli przedmiotów (Lewo / Prawo)
  if (digitalRead(BTN_LEFT) == LOW) {
    currentShopItem--;
    if (currentShopItem < 0) currentShopItem = totalShopItems - 1;
    delay(200); 
  }
  
  if (digitalRead(BTN_RIGHT) == LOW) {
    currentShopItem++;
    if (currentShopItem >= totalShopItems) currentShopItem = 0;
    delay(200); 
  }

  // Obsługa zatwierdzenia akcji na wybranym przedmiocie (Kupno lub użycie)
  if (digitalRead(BTN_ACCEPT) == LOW) {
    if (!ownsEmote[currentShopItem]) {
      // Logika weryfikacji ilości monet i odejmowania salda przy zakupie
      if (globalCoins >= currentPrice) {
        globalCoins -= currentPrice;
        ownsEmote[currentShopItem] = true;
        
        display.fillRect(38, 38, 60, 10, SH110X_BLACK);
        display.setCursor(42, 38);
        display.print(F("KUPIONE!"));
        display.display();
        delay(1000);
      } else {
        // Komunikat braku środków
        display.fillRect(38, 38, 70, 10, SH110X_BLACK);
        display.setCursor(25, 38);
        display.print(F("ZA MALO MONET"));
        display.display();
        delay(1500);
      }
    } else {
      // Jeśli przedmiot jest już kupiony - wywołanie przypisanej do niego akcji (animacji)
      playEmoteAnimation(currentShopItem);
    }
    
    while(digitalRead(BTN_ACCEPT) == LOW); 
    delay(50);
  }
}

// ==========================================
// PETLA GRY: PAC-MAN
// ==========================================
void loopPacman() {
  // Blok obsługujący główne menu gry - wybór trudności
  if (inMenu) {
    display.clearDisplay();
    drawCoins(); 

    // Renderowanie elementów wyboru poziomu, podświetlenie (inwersja kolorów) zaznaczonej opcji
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    display.setCursor(19, 10);
    display.print(F("WYBIERZ POZIOM:"));

    if (selectedLevel == 0) {
      display.fillRect(10, 24, 45, 16, SH110X_WHITE); 
      display.setTextColor(SH110X_BLACK);             
    } else {
      display.drawRect(10, 24, 45, 16, SH110X_WHITE); 
      display.setTextColor(SH110X_WHITE);
    }
    display.setCursor(17, 28);
    display.print(F("LATWY"));

    if (selectedLevel == 1) {
      display.fillRect(65, 24, 50, 16, SH110X_WHITE); 
      display.setTextColor(SH110X_BLACK);             
    } else {
      display.drawRect(65, 24, 50, 16, SH110X_WHITE);
      display.setTextColor(SH110X_WHITE);
    }
    display.setCursor(72, 28);
    display.print(F("TRUDNY"));
    
    if (selectedLevel == 2) {
      display.fillRect(28, 44, 72, 16, SH110X_WHITE); 
      display.setTextColor(SH110X_BLACK);             
    } else {
      display.drawRect(28, 44, 72, 16, SH110X_WHITE);
      display.setTextColor(SH110X_WHITE);
    }
    display.setCursor(34, 48);
    display.print(F("NIEMOZLIWY"));

    display.display();
    delay(50); 

    // Nasłuchiwanie wejść dla menu
    if (digitalRead(BTN_LEFT) == LOW) { 
      selectedLevel--; 
      if (selectedLevel < 0) selectedLevel = 2; 
      delay(200); 
    }
    if (digitalRead(BTN_RIGHT) == LOW) { 
      selectedLevel++; 
      if (selectedLevel > 2) selectedLevel = 0; 
      delay(200); 
    }
    if (digitalRead(BTN_ACCEPT) == LOW) { 
      inMenu = false; 
      startGame(); 
      while(digitalRead(BTN_ACCEPT) == LOW); 
      delay(50); 
    }
    return; 
  }

  // Szybkie wyjście po porażce lub wygranej powracające do menu Pac-Mana
  if (gameOver || gameWon) {
    if (digitalRead(BTN_ACCEPT) == LOW) { 
      inMenu = true;
      gameOver = false;
      gameWon = false;
      while(digitalRead(BTN_ACCEPT) == LOW); 
      delay(50); 
    }
    return; 
  }

  // --- MECHANIKA ROZGRYWKI PAC-MAN ---
  
  // Zapisanie pożądanego kierunku gracza na podstawie wejścia
  if (digitalRead(BTN_RIGHT) == LOW) next_dir = 0;
  if (digitalRead(BTN_DOWN) == LOW)  next_dir = 1;
  if (digitalRead(BTN_LEFT) == LOW)  next_dir = 2;
  if (digitalRead(BTN_UP) == LOW)    next_dir = 3;

  // Logika weryfikująca potencjalny ruch we wskazanym kierunku - detekcja kolizji ze ścianą (1)
  int test_r = pac_r, test_c = pac_c;
  if (next_dir == 0) test_c++;
  else if (next_dir == 1) test_r++;
  else if (next_dir == 2) test_c--;
  else if (next_dir == 3) test_r--;

  // Jeżeli pole jest wolne - zmień kierunek; jeżeli nie - próbuj kontynuować marsz z ostatnio wciśniętym kierunkiem
  if (maze[test_r][test_c] != 1) {
    pac_dir = next_dir;
    pac_r = test_r;
    pac_c = test_c;
  } else {
    test_r = pac_r; test_c = pac_c;
    if (pac_dir == 0) test_c++;
    else if (pac_dir == 1) test_r++;
    else if (pac_dir == 2) test_c--;
    else if (pac_dir == 3) test_r--;
    
    if (maze[test_r][test_c] != 1) {
      pac_r = test_r;
      pac_c = test_c;
    }
  }

  // Logika zjadania kropki (2), aktualizacja punktów i weryfikacja warunków wygranej oraz nagrody pieniężnej
  if (maze[pac_r][pac_c] == 2) {
    maze[pac_r][pac_c] = 0;
    score += 10;
    dotsLeft--; 
    if (dotsLeft == 0) {
      gameWon = true; 
      if (selectedLevel == 0) globalCoins += 5;
      else if (selectedLevel == 1) globalCoins += 10;
      else if (selectedLevel == 2) globalCoins += 100;
    }
  }

  // Detekcja wczesnej kolizji gracza z duchem
  for (int g = 0; g < activeGhosts; g++) {
    if (pac_r == ghost_r[g] && pac_c == ghost_c[g]) gameOver = true;
  }

  // Proste algorytmy poruszania (AI) dla duchów
  if (!gameOver) {
    int g_dirs[4][2] = {{0,1}, {1,0}, {0,-1}, {-1,0}}; 
    for (int g = 0; g < activeGhosts; g++) {
      int valid_dirs[4];
      int num_valid = 0;
      int opposite_dir = (ghost_dir[g] + 2) % 4; // Duch nie może zawrócić o 180 stopni, chyba że musi
      
      // Sprawdzanie dozwolonych kierunków - ignorowanie ścian oraz innych duchów (by się na siebie nie nakładały)
      for(int i=0; i<4; i++) {
        int test_r = ghost_r[g] + g_dirs[i][0];
        int test_c = ghost_c[g] + g_dirs[i][1];
        
        if (maze[test_r][test_c] != 1) { 
          bool isOccupied = false;
          for (int other = 0; other < activeGhosts; other++) {
            if (g != other && ghost_r[other] == test_r && ghost_c[other] == test_c) {
              isOccupied = true;
              break;
            }
          }
          if (!isOccupied && i != opposite_dir) {
            valid_dirs[num_valid++] = i;
          }
        }
      }
      
      // Wybór losowego poprawnego ruchu; wymuszone zawrócenie występuje w przypadku wejścia w ślepą uliczkę
      int move = -1;
      if (num_valid > 0) {
        move = valid_dirs[random(num_valid)];
      } else {
        int back_r = ghost_r[g] + g_dirs[opposite_dir][0];
        int back_c = ghost_c[g] + g_dirs[opposite_dir][1];
        bool backOccupied = false;
        
        for (int other = 0; other < activeGhosts; other++) {
          if (g != other && ghost_r[other] == back_r && ghost_c[other] == back_c) {
            backOccupied = true; 
            break;
          }
        }
        
        if (!backOccupied && maze[back_r][back_c] != 1) {
           move = opposite_dir;
        }
      }
      
      // Zastosowanie wyliczonego ruchu do pozycji duszka
      if (move != -1) {
        ghost_dir[g] = move;
        ghost_r[g] += g_dirs[move][0];
        ghost_c[g] += g_dirs[move][1];
      }
    }
  }

  // Weryfikacja późnej kolizji z duchem (po przeliczeniu ich posunięcia)
  for (int g = 0; g < activeGhosts; g++) {
    if (pac_r == ghost_r[g] && pac_c == ghost_c[g]) gameOver = true;
  }

  // --- RENDEROWANIE SCENY PAC-MANA ---
  display.clearDisplay();

  // Rysowanie poszczególnych elementów siatki mapy - ścian oraz kropek
  for(int r=0; r<7; r++){
    for(int c=0; c<16; c++){
      if (maze[r][c] == 1) { 
        display.drawRect(c*8, r*8, 8, 8, SH110X_WHITE);
      } else if (maze[r][c] == 2) { 
        display.drawPixel(c*8+3, r*8+3, SH110X_WHITE);
        display.drawPixel(c*8+4, r*8+3, SH110X_WHITE);
        display.drawPixel(c*8+3, r*8+4, SH110X_WHITE);
        display.drawPixel(c*8+4, r*8+4, SH110X_WHITE);
      }
    }
  }

  // Graficzne rysowanie figur poszczególnych duchów
  for (int g = 0; g < activeGhosts; g++) {
    int gx = ghost_c[g] * 8 + 4;
    int gy = ghost_r[g] * 8 + 4;
    display.fillRoundRect(gx-3, gy-3, 7, 7, 2, SH110X_WHITE);
    display.fillRect(gx-3, gy, 7, 4, SH110X_WHITE); 
    display.drawPixel(gx-1, gy-1, SH110X_BLACK); 
    display.drawPixel(gx+1, gy-1, SH110X_BLACK);
  }

  // Graficzne rysowanie postaci gracza z obsługą "kłapiącej" buzi zorientowanej względem kierunku ruchu
  int px = pac_c * 8 + 4;
  int py = pac_r * 8 + 4;
  display.fillCircle(px, py, 3, SH110X_WHITE);
  
  mouthOpen = !mouthOpen; 
  if (mouthOpen) {
    if (pac_dir == 0) display.fillTriangle(px, py, px+4, py-3, px+4, py+3, SH110X_BLACK); 
    else if (pac_dir == 1) display.fillTriangle(px, py, px-3, py+4, px+3, py+4, SH110X_BLACK); 
    else if (pac_dir == 2) display.fillTriangle(px, py, px-4, py-3, px-4, py+3, SH110X_BLACK); 
    else if (pac_dir == 3) display.fillTriangle(px, py, px-3, py-4, px+3, py-4, SH110X_BLACK); 
  }

  // Wypisywanie punktacji
  display.setTextColor(SH110X_WHITE); 
  display.setCursor(0, 56);
  display.print(F("Score: "));
  display.print(score);

  // Ekrany nakładki (overlay) dla stanów końca gry - Porażki lub Zwycięstwa
  if (gameOver) {
    display.fillRect(14, 16, 100, 30, SH110X_BLACK);
    display.drawRect(14, 16, 100, 30, SH110X_WHITE);
    display.setCursor(37, 20); display.print(F("GAME OVER"));
    display.setCursor(34, 32); display.print(F("Nacisnij 4")); 
  } else if (gameWon) {
    display.fillRect(14, 12, 100, 40, SH110X_BLACK);
    display.drawRect(14, 12, 100, 40, SH110X_WHITE);
    display.setCursor(40, 16); display.print(F("WYGRALES"));
    
    // Wyświetlanie komunikatu z odebraną nagrodą (zależną od trudności)
    if (selectedLevel == 0) { display.setCursor(34, 28); display.print(F("+ 5 MONET!")); }
    else if (selectedLevel == 1) { display.setCursor(31, 28); display.print(F("+ 10 MONET!")); }
    else if (selectedLevel == 2) { display.setCursor(25, 28); display.print(F("+ 100 MONET!")); }
    
    display.setCursor(34, 40); display.print(F("Nacisnij 4")); 
  }
  
  // Wrzucenie bufora na ekran fizyczny i opóźnienie regulujące płynność/szybkość gry
  display.display();
  delay(gameDelay); 
}

// ==========================================
// PETLA GRY: JEDNORĘKI BANDYTA
// ==========================================
void loopSlotMachine() {
  // Blok reagujący na przycisk "pociągnięcia za dźwignię" (rozpoczęcie losowania)
  if (digitalRead(BUTTON_PIN) == LOW) {
    
    // Logika weryfikująca posiadanie odpowiedniego budżetu pod zakład gry
    if (globalCoins < 20) {
      display.clearDisplay();
      display.setTextSize(1);
      display.setCursor(25, 28);
      display.print(F("BRAK SRODKOW!"));
      display.setCursor(19, 40);
      display.print(F("GRAJ W PAC-MANA"));
      display.display();
      delay(2000);
      
      showSlotWelcome();
      while(digitalRead(BUTTON_PIN) == LOW); 
      return;
    }
    
    // Potrącenie kosztu gry z ogólnego konta
    globalCoins -= 20; 
    
    int r1 = 0, r2 = 0, r3 = 0;
    
    // Pętla odtwarzająca wizualny efekt kręcenia się trzech niezależnych bębnów
    // i wprowadzająca spowolnienie aby nadać ciężar "maszynie"
    for (int i = 0; i < 40; i++) {
      display.clearDisplay();
      drawCoins(); 
      
      display.setTextSize(1);
      display.setCursor(0, 0); 
      display.print(F("LOSUJE...")); 
      
      // Zatrzymywanie kolejnych bębnów w czasie (efekt symulowanego opadania / blokowania)
      if (i < 15) r1 = random(5);
      if (i < 25) r2 = random(5);
      if (i < 35) r3 = random(5);
      
      // Renderowanie losowanych pozycji
      drawReel(10, 15, getSymbol(r1));
      drawReel(48, 15, getSymbol(r2));
      drawReel(86, 15, getSymbol(r3));
      
      display.display();
      delay(30 + (i * 2)); 
    }
    
    // Logika oceny wygranej / systemu premiowania konkretnych układów wylosowanych znaków
    int winnings = 0;
    char sym1 = getSymbol(r1);
    char sym2 = getSymbol(r2);
    char sym3 = getSymbol(r3);
    
    // Sprawdzanie potrójnych symboli oraz specjalnego, najwyższego jackpota dla siódemek
    if (sym1 == sym2 && sym2 == sym3) {
      if (sym1 == '7') {
        winnings = 1000;
      } else {
        winnings = 80;
      }
    // Przeliczanie tzw. "małej wygranej" na podstawie częściowego trafienia
    } else if (sym1 == sym2 || sym2 == sym3 || sym1 == sym3) {
      winnings = 20;
    }
    
    // Przyznanie wygranej
    globalCoins += winnings; 
    
    // Blok podsumowania losowania, renderowanie wyników uiszczonego zakładu
    display.clearDisplay();
    drawCoins(); 
    
    display.setTextSize(2); 
    if (winnings == 1000) {
      display.setCursor(4, 24); display.print(F("JACKPOT!!!")); 
    } else if (winnings == 80) {
      display.setCursor(16, 24); display.print(F("WYGRANA!")); 
    } else if (winnings == 20) {
      display.setCursor(40, 14); display.print(F("MALA")); 
      display.setCursor(22, 34); display.print(F("WYGRANA")); 
    } else {
      display.setCursor(10, 24); display.print(F("PRZEGRANA")); 
    }
    
    display.setTextSize(1);
    display.setCursor(16, 54);
    display.print(F("Zagraj ponownie!"));
    display.display();
    
    delay(1000);
    // Zabezpieczenie ciągłego wciskania - zapobiega przypadkowym, podwójnym losowaniom
    while(digitalRead(BUTTON_PIN) == LOW);
  }
}
Youtube
Tagi
#gra #pacman #pac-man #PAC-MAN #pacmann