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.
1. Płytka Arduino UNO
2. Płytka SIC
3. Wyświetlacz
4. 6 przycisków
5. Obudowa
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.
// ==========================================
// 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);
}
}