Projekt „Doomers Minesweeper” to nasza własna implementacja klasycznej gry Saper, stworzona na platformę Arduino. Zdecydowaliśmy się na ten projekt, ponieważ obaj jesteśmy dużymi fanami tej gry i chcieliśmy odtworzyć jej klimat w wersji sprzętowej, zachowując charakterystyczny styl retro.
1. Płytka Arduino UNO
2. Shield SIC Game Console
3. Przewód do Arduino
Z NASA lub podobny
Naszym celem było nie tylko odwzorowanie podstawowej mechaniki gry, ale również dodanie własnych usprawnień i efektów wizualnych.
Jednym z największych wyzwań było zaprojektowanie całego systemu działania gry w oparciu o maszynę stanów oraz zapewnienie płynnej obsługi wielu ekranów, takich jak menu, gra, pauza, animacje czy tabela wyników. Szczególnie trudne okazało się stworzenie systemu zapisu wyników dla różnych poziomów trudności oraz ich przechowywanie w pamięci EEPROM, tak aby nie znikały po wyłączeniu zasilania.
Od strony programistycznej wyzwaniem była również optymalizacja pamięci RAM, która w Arduino jest bardzo ograniczona. Zastosowaliśmy technikę przechowywania danych w postaci bitmask zamiast pełnych struktur oraz algorytm FIFO do obsługi odkrywania pustych pól, co znacząco poprawiło wydajność i stabilność działania gry.
Projekt wyróżnia się unikalnym stylem graficznym oraz zestawem własnoręcznie zaprojektowanych ikon, takich jak mina, flaga czy puchar. Dodaliśmy również animacje, m.in. efekt wybuchu po przegranej oraz animację fajerwerków po wygranej, co znacząco poprawia wrażenia z rozgrywki.
Gra oferuje trzy poziomy trudności (łatwy, średni i trudny), system liczenia czasu oraz tabelę najlepszych wyników. Zaimplementowaliśmy także funkcję pauzy dostępnej w trakcie gry oraz mechanikę „chordingu”, znaną z klasycznej wersji Sapera, która przyspiesza odkrywanie pól.
Projekt był inspirowany klasycznym Saperem znanym z systemu Windows i staraliśmy się zachować jego klimat, jednocześnie rozwijając go o nowe funkcje i ulepszenia.
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <EEPROM.h>
// ===== GRAFIKI (BITMAPY W PROGMEM) =====
// Ikona Flagi 8x8 (Wyśrodkowana)
const unsigned char bmp_flag[] PROGMEM = {
0x00, // 00000000 - pusty margines
0x3C, // 00111100 - góra flagi
0x3E, // 00111110 - środek flagi
0x3C, // 00111100 - dół flagi
0x20, // 00100000 - maszt
0x20, // 00100000 - maszt
0x70, // 01110000 - podstawa masztu
0x00 // 00000000 - pusty margines
};
// Ikona Pucharu 16x16 (Czysty, klasyczny kształt z uszami, bez cyfr)
const unsigned char bmp_trophy[] PROGMEM = {
0x07, 0xE0, // 00000111 11100000 - Góra czaszy
0x1F, 0xF8, // 00011111 11111000
0x6F, 0xF6, // 01101111 11110110 - Uszy start (z przerwami)
0x8F, 0xF1, // 10001111 11110001 - Uszy szeroko
0x8F, 0xF1, // 10001111 11110001
0x8F, 0xF1, // 10001111 11110001
0x6F, 0xF6, // 01101111 11110110 - Uszy koniec
0x1F, 0xF8, // 00011111 11111000 - Zakończenie uszu, dół czaszy
0x0F, 0xF0, // 00001111 11110000 - Zwężenie czaszy
0x07, 0xE0, // 00000111 11100000
0x03, 0xC0, // 00000011 11000000 - Łączenie z nóżką
0x01, 0x80, // 00000001 10000000 - Nóżka
0x03, 0xC0, // 00000011 11000000 - Podstawa start
0x0F, 0xF0, // 00001111 11110000
0x1F, 0xF8, // 00011111 11111000
0x3F, 0xFC // 00111111 11111100 - Płaski dół podstawy
};
// Ikona Bomby 8x8 (Okrągły korpus + lont + pojedyncza iskra po prawej)
const unsigned char bmp_mine[] PROGMEM = {
0x01, // 00000001 - Iskra (tylko jeden punkt na samym krańcu)
0x02, // 00000010 - Lont (lekko przesunięty, żeby łączył się z iskrą po skosie)
0x1C, // 00011100 - Szyjka bomby
0x36, // 00110110 - Korpus z odblaskiem (ten '0' w środku to połysk)
0x3E, // 00111110 - Środek korpusu
0x3E, // 00111110 - Dół korpusu
0x1C, // 00011100 - Spód korpusu
0x00 // 00000000
};
// ===== OLED =====
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
// ===== PRZYCISKI =====
#define BTN_UP 7
#define BTN_DOWN 9
#define BTN_LEFT 10
#define BTN_RIGHT 8
#define BTN_ENTER 4
#define BTN_BACK 5
// ===== PLANSZA =====
#define WIDTH 16
#define HEIGHT 8
#define SIZE 128
// ===== BITMASKI (oszczędność RAM) =====
uint8_t mines[16]; // 128 bitów
uint8_t revealed[16];
uint8_t flags[16];
int bestTimes[3][3];
unsigned long gameStartTime;
int lastScore = 0;
// ===== STAN GRY =====
enum GameState { MENU, PLAYING, GAMEOVER, WIN, PAUSE, START_ANIM, EXPLOSION, GAMEOVER_ANIM, WIN_ANIM, SCORES_ALL, SCORES_LOCAL};
GameState currentState = MENU;
uint8_t cursorX = 0;
uint8_t cursorY = 0;
bool firstClick = true;
bool chordingEnabled = true;
int animRadius = 0;
int blastX, blastY;
int explosionRadius = 0;
int goFrame = 0;
int winFrame = 0;
// ===== TRUDNOŚĆ =====
uint8_t difficulty = 0; // 0 = Łatwy, 1 = Średni, 2 = Trudny
const uint8_t mineCounts[3] = { 10, 20, 30 };
uint8_t totalMines = 10;
// ===== TIMERY STEROWANIA =====
unsigned long lastInputTime = 0;
const unsigned long REPEAT_DELAY = 120;
const unsigned long ACTION_DELAY = 200;
// ===== OPERACJE BITOWE =====
bool getBit(uint8_t *arr, uint8_t i) {
return (arr[i >> 3] >> (i & 7)) & 1;
}
void setBit(uint8_t *arr, uint8_t i) {
arr[i >> 3] |= (1 << (i & 7));
}
void clearBit(uint8_t *arr, uint8_t i) {
arr[i >> 3] &= ~(1 << (i & 7));
}
uint8_t idx(uint8_t x, uint8_t y) {
return y * WIDTH + x;
}
// ===== LICZENIE SĄSIADÓW =====
uint8_t countNeighbors(uint8_t x, uint8_t y) {
uint8_t count = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && ny >= 0 && nx < WIDTH && ny < HEIGHT) {
if (getBit(mines, idx(nx, ny))) count++;
}
}
}
return count;
}
// ===== LICZENIE FLAG =====
uint8_t countFlags() {
uint8_t count = 0;
for (uint8_t i = 0; i < SIZE; i++) {
if (getBit(flags, i)) count++;
}
return count;
}
// ===== GENEROWANIE =====
void generateBoard(uint8_t safeX, uint8_t safeY) {
for (int i = 0; i < 16; i++) {
mines[i] = 0;
revealed[i] = 0;
flags[i] = 0;
}
uint8_t placed = 0;
while (placed < totalMines) {
uint8_t x = random(WIDTH);
uint8_t y = random(HEIGHT);
if (abs(x - safeX) <= 1 && abs(y - safeY) <= 1) continue;
uint8_t i = idx(x, y);
if (!getBit(mines, i)) {
setBit(mines, i);
placed++;
}
}
}
// ===== REVEAL (ITERACYJNY FLOOD FILL) =====
void reveal(uint8_t startX, uint8_t startY) {
if (startX >= WIDTH || startY >= HEIGHT) return;
uint8_t startIdx = idx(startX, startY);
if (getBit(revealed, startIdx) || getBit(flags, startIdx)) return;
setBit(revealed, startIdx);
if (getBit(mines, startIdx)) {
blastX = startX * 8 + 4;
blastY = startY * 8 + 4;
explosionRadius = 0;
display.invertDisplay(true);
currentState = EXPLOSION;
return;
}
if (countNeighbors(startX, startY) > 0) return;
uint8_t queue[128];
uint8_t head = 0;
uint8_t tail = 0;
queue[tail++] = startIdx;
while (head < tail) {
uint8_t currentIdx = queue[head++];
uint8_t cx = currentIdx % WIDTH;
uint8_t cy = currentIdx / WIDTH;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = cx + dx;
int ny = cy + dy;
if (nx >= 0 && ny >= 0 && nx < WIDTH && ny < HEIGHT) {
uint8_t nIdx = idx(nx, ny);
if (!getBit(revealed, nIdx) && !getBit(flags, nIdx)) {
setBit(revealed, nIdx);
if (countNeighbors(nx, ny) == 0) {
queue[tail++] = nIdx;
}
}
}
}
}
}
}
// ===== CHORDING =====
void chord(uint8_t x, uint8_t y) {
uint8_t i = idx(x, y);
if (!getBit(revealed, i)) return;
uint8_t minesAround = countNeighbors(x, y);
if (minesAround == 0) return;
uint8_t flagsAround = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && ny >= 0 && nx < WIDTH && ny < HEIGHT) {
if (getBit(flags, idx(nx, ny))) {
flagsAround++;
}
}
}
}
if (flagsAround == minesAround) {
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && ny >= 0 && nx < WIDTH && ny < HEIGHT) {
uint8_t nIdx = idx(nx, ny);
if (!getBit(flags, nIdx) && !getBit(revealed, nIdx)) {
reveal(nx, ny);
}
}
}
}
}
}
// ===== RYSOWANIE =====
void drawWinAnimation() {
display.clearDisplay();
for(int i = 0; i < 4; i++) {
int cycleLength = 60;
int localFrame = winFrame + (i * 15);
int currentCycle = localFrame / cycleLength;
int cycleStep = localFrame % cycleLength;
randomSeed(currentCycle * 100 + i);
bool isLeft = (i % 2 == 0);
int startX = isLeft ? random(2, 16) : random(112, 126);
int explodeY = random(5, 25);
int startY = 64;
if (cycleStep < 20) {
int currentY = map(cycleStep, 0, 19, startY, explodeY);
display.drawPixel(startX, currentY, SH110X_WHITE);
display.drawPixel(startX, currentY + 1, SH110X_WHITE);
}
else if (cycleStep < 40) {
int r = (cycleStep - 20) / 2;
if (r > 0) {
int px[8] = {startX, startX, startX-r, startX+r, startX-r, startX+r, startX-r, startX+r};
int py[8] = {explodeY-r, explodeY+r, explodeY, explodeY, explodeY-r, explodeY-r, explodeY+r, explodeY+r};
for(int p = 0; p < 8; p++) {
if (px[p] < 20 || px[p] > 108) {
display.drawPixel(px[p], py[p] + (r / 3), SH110X_WHITE);
}
}
}
}
}
display.drawBitmap(56, 5, bmp_trophy, 16, 16, SH110X_WHITE);
int jumpY = sin(winFrame * 0.2) * 2;
display.setTextSize(2);
display.setCursor(22, 28 + jumpY);
display.print(F("YOU WIN"));
display.setTextSize(1);
if ((millis() / 500) % 2 == 0) {
display.setCursor(18, 55);
display.print(F(" BACK"));
}
display.setCursor(58, 55);
display.print(F("to menu"));
display.setTextSize(1);
display.setCursor(30, 46);
display.print(F("Time: "));
display.print(lastScore);
display.print(F("s"));
display.display();
}
void drawOpeningAnimation() {
display.clearDisplay();
int centerX = 64;
int centerY = 32;
for (uint8_t x = 0; x < WIDTH; x++) {
for (uint8_t y = 0; y < HEIGHT; y++) {
int px = x * 8;
int py = y * 8;
int cellX = px + 4;
int cellY = py + 4;
int dist = abs(cellX - centerX) + abs(cellY - centerY);
if (dist < animRadius) {
uint8_t i = idx(x, y);
if (getBit(flags, i)) {
display.drawBitmap(px, py, bmp_flag, 8, 8, SH110X_WHITE);
} else {
display.fillRect(px + 2, py + 3, 4, 4, SH110X_WHITE);
}
}
}
}
display.display();
}
void drawExplosion() {
display.drawCircle(blastX, blastY, explosionRadius, SH110X_WHITE);
if (explosionRadius > 5)
display.drawCircle(blastX, blastY, explosionRadius - 5, SH110X_WHITE);
for(int i=0; i<10; i++) {
int rx = blastX + random(-explosionRadius, explosionRadius);
int ry = blastY + random(-explosionRadius, explosionRadius);
display.drawPixel(rx, ry, SH110X_WHITE);
}
display.display();
}
void drawBoard() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
for (uint8_t x = 0; x < WIDTH; x++) {
for (uint8_t y = 0; y < HEIGHT; y++) {
uint8_t i = idx(x, y);
uint8_t px = x * 8;
uint8_t py = y * 8;
if (cursorX == x && cursorY == y) {
display.drawRect(px, py, 8, 8, SH110X_WHITE);
}
display.setCursor(px, py);
if (getBit(revealed, i)) {
if (getBit(mines, i)) {
display.drawBitmap(px, py, bmp_mine, 8, 8, SH110X_WHITE);
} else {
uint8_t n = countNeighbors(x, y);
if (n > 0) {
display.setCursor(px + 2, py + 1);
display.print(n);
}
}
} else {
if (getBit(flags, i)) {
display.drawBitmap(px, py, bmp_flag, 8, 8, SH110X_WHITE);
} else {
display.fillRect(px + 2, py + 2, 4, 4, SH110X_WHITE);
}
}
}
}
display.display();
}
void drawMenu() {
display.clearDisplay();
display.setTextColor(SH110X_WHITE);
long curMillis = millis();
int scrollSpeed = 3000;
int blockHeight = 35;
int scrollY = (curMillis % scrollSpeed) * blockHeight / scrollSpeed;
for (int i = 0; i < 2; i++) {
int yPos = scrollY - (i * blockHeight);
display.setTextSize(2);
display.setCursor(22, yPos);
display.print(F("DOOMERS"));
display.setTextSize(1);
display.setCursor(31, yPos + 18);
display.print(F("MINESWEEPER"));
}
display.fillRect(0, 31, 128, 33, SH110X_BLACK);
int danceSpeed = 300;
int amplitude = 3;
int offset = sin(curMillis * 2 * PI / (danceSpeed * 10)) * amplitude;
int dashY = 36;
display.setTextSize(1);
display.setCursor(0, dashY + offset);
display.print(F("-"));
display.setCursor(122, dashY - offset);
display.print(F("-"));
if (difficulty == 0) {
display.setCursor(12, dashY);
display.print(F("Level: EASY (10)"));
}
else if (difficulty == 1) {
display.setCursor(7, dashY);
display.print(F("Level: MEDIUM (20)"));
}
else if (difficulty == 2) {
display.setCursor(7, dashY);
display.print(F("Level: HARD (30)"));
}
if ((millis() / 500) % 2 == 0) {
display.setTextSize(1);
display.setCursor(10, 54);
display.print(F(" ENTER"));
}
display.setCursor(12, 54);
display.print(F(" to start"));
display.display();
}
void drawPause() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(25, 5);
display.print(F("--- PAUSE ---"));
display.setCursor(5, 25);
display.print(F("Mines left: "));
int minyZostalo = totalMines - countFlags();
display.print(minyZostalo);
display.setCursor(5, 40);
display.print(F("Chording: "));
if (chordingEnabled) display.print(F("ON"));
else display.print(F("OFF"));
display.setCursor(5, 55);
display.print(F("ENTER:Set BACK:Resume"));
display.display();
}
void drawGameOverAnimation() {
display.clearDisplay();
if (goFrame < 100) {
float radius = 50.0 - ((float)goFrame * 0.5);
float angle = (float)goFrame * 0.2;
int x = 64 + cos(angle) * radius - 27;
int y = 32 + sin(angle) * radius - 4;
display.setTextSize(1);
display.setCursor(x, y);
display.print(F("GAME OVER"));
}
else {
randomSeed(goFrame / 10);
for(int i=0; i<4; i++) {
int bx = random(0, 120);
int by;
if (random(0, 2) == 0) {
by = random(0, 4);
} else {
by = random(42, 45);
}
int bSize = (goFrame / 5) % 8;
display.drawBitmap(bx, by, bmp_mine, 8, 8, SH110X_WHITE);
if (bSize > 2) {
display.drawCircle(bx+4, by+4, bSize, SH110X_WHITE);
}
}
if ((millis() / 1250) % 2 == 0) {
display.setTextSize(2);
display.setCursor(10, 25);
display.print(F("GAME OVER"));
}
display.setTextSize(1);
if ((millis() / 500) % 2 == 0) {
display.setCursor(18, 54);
display.print(F(" BACK"));
}
display.setCursor(58, 54);
display.print(F("to restart"));
}
display.display();
}
void drawScoresAll() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(18, 0);
display.print(F("HIGH SCORES"));
display.drawFastHLine(0, 10, 128, SH110X_WHITE);
for (int i = 0; i < 3; i++) {
display.setCursor(10, 20 + (i * 10));
if (i == 0) display.print(F("EASY: "));
else if (i == 1) display.print(F("MEDIUM:"));
else display.print(F("HARD:"));
display.setCursor(70, 20 + (i * 10));
// Wyświetlamy tylko pozycję [0] czyli najlepszy wynik z danego poziomu
if (bestTimes[i][0] == 999) display.print(F("---"));
else {
display.print(bestTimes[i][0]);
display.print(F(" sec"));
}
}
// Część mrugająca
if ((millis() / 500) % 2 == 0) {
display.setCursor(15, 54);
display.print(F(" BACK"));
}
// Część stała
display.setCursor(55, 54);
display.print(F("to menu"));
display.display();
}
// Wywoływane z WIN - Trzy najlepsze wyniki dla OBECNEGO poziomu
void drawScoresLocal() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(20, 0);
if (difficulty == 0) display.print(F("TOP 3 - EASY"));
else if (difficulty == 1) display.print(F("TOP 3 - MEDIUM"));
else display.print(F("TOP 3 - HARD"));
display.drawFastHLine(0, 10, 128, SH110X_WHITE);
for (int i = 0; i < 3; i++) {
display.setCursor(35, 20 + (i * 10));
display.print(i + 1);
display.print(F(". "));
// Zmienna 'difficulty' mówi nam na jakim poziomie właśnie graliśmy
if (bestTimes[difficulty][i] == 999) display.print(F("---"));
else {
display.print(bestTimes[difficulty][i]);
display.print(F(" sec"));
}
}
// Część mrugająca
if ((millis() / 500) % 2 == 0) {
display.setCursor(15, 54);
display.print(F(" BACK"));
}
// Część stała
display.setCursor(55, 54);
display.print(F("to menu"));
display.display();
}
// ===== INPUT =====
void handleInput() {
unsigned long currentMillis = millis();
if (currentMillis - lastInputTime >= REPEAT_DELAY) {
bool moved = false;
if (digitalRead(BTN_UP) == LOW) {
if (cursorY > 0) cursorY--;
moved = true;
} else if (digitalRead(BTN_DOWN) == LOW) {
if (cursorY < HEIGHT - 1) cursorY++;
moved = true;
} else if (digitalRead(BTN_LEFT) == LOW) {
if (cursorX > 0) cursorX--;
moved = true;
} else if (digitalRead(BTN_RIGHT) == LOW) {
if (cursorX < WIDTH - 1) cursorX++;
moved = true;
}
if (moved) {
lastInputTime = currentMillis;
return;
}
}
if (currentMillis - lastInputTime >= ACTION_DELAY) {
bool actionTaken = false;
// --- LOGIKA ENTER ---
if (digitalRead(BTN_ENTER) == LOW) {
if (firstClick) {
generateBoard(cursorX, cursorY);
firstClick = false;
}
uint8_t currentIdx = idx(cursorX, cursorY);
if (getBit(revealed, currentIdx) && chordingEnabled) {
chord(cursorX, cursorY);
} else {
reveal(cursorX, cursorY);
}
checkWin();
while (digitalRead(BTN_ENTER) == LOW) {
delay(10);
}
actionTaken = true;
}
// --- LOGIKA BACK ---
else if (digitalRead(BTN_BACK) == LOW) {
int holdTime = 0;
while (digitalRead(BTN_BACK) == LOW) {
delay(10);
holdTime += 10;
if (holdTime > 500) {
currentState = PAUSE;
while (digitalRead(BTN_BACK) == LOW);
return;
}
}
uint8_t i = idx(cursorX, cursorY);
if (getBit(flags, i)) clearBit(flags, i);
else setBit(flags, i);
actionTaken = true;
}
if (actionTaken) {
lastInputTime = currentMillis;
}
}
}
// ===== CZYSZCZENIE EKRANU =====
void clearBoard() {
for (int i = 0; i < 16; i++) {
mines[i] = 0;
revealed[i] = 0;
flags[i] = 0;
}
}
void checkWin() {
uint8_t revealedCount = 0;
for (uint8_t i = 0; i < SIZE; i++) {
if (getBit(revealed, i)) {
revealedCount++;
}
}
if (revealedCount == (SIZE - totalMines)) {
// ZATRZYMANIE CZASU I ZAPIS
lastScore = (millis() - gameStartTime) / 1000;
saveScore(lastScore);
winFrame = 0;
currentState = WIN_ANIM;
}
}
void saveScore(int time) {
for (int i = 0; i < 3; i++) {
// Jeśli nowy czas jest lepszy niż obecny na pozycji 'i'
if (time < bestTimes[difficulty][i]) {
// Przesuń wszystkie gorsze wyniki o jedno miejsce w dół
for (int j = 2; j > i; j--) {
bestTimes[difficulty][j] = bestTimes[difficulty][j-1];
EEPROM.put((difficulty * 3 + j) * sizeof(int), bestTimes[difficulty][j]);
}
// Wpisz nowy rekord i zakończ
bestTimes[difficulty][i] = time;
EEPROM.put((difficulty * 3 + i) * sizeof(int), bestTimes[difficulty][i]);
break;
}
}
}
// ===== SETUP =====
void setup() {
display.begin(0x3C, true);
display.setRotation(2);
display.clearDisplay();
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
pinMode(BTN_ENTER, INPUT_PULLUP);
pinMode(BTN_BACK, INPUT_PULLUP);
randomSeed(analogRead(A0));
for (int d = 0; d < 3; d++) {
for (int r = 0; r < 3; r++) {
EEPROM.get((d * 3 + r) * sizeof(int), bestTimes[d][r]);
if (bestTimes[d][r] <= 0 || bestTimes[d][r] > 999) {
bestTimes[d][r] = 999;
}
}
}
}
// ===== LOOP =====
void loop() {
switch (currentState) {
case GAMEOVER_ANIM:
drawGameOverAnimation();
goFrame++;
delay(15);
if (digitalRead(BTN_BACK) == LOW) {
currentState = MENU;
firstClick = true;
clearBoard();
cursorX = 0;
cursorY = 0;
delay(200);
}
break;
case EXPLOSION:
drawExplosion();
explosionRadius += 1;
if (explosionRadius > 80) {
display.invertDisplay(false);
for (uint8_t i = 0; i < SIZE; i++) {
if (getBit(mines, i)) setBit(revealed, i);
}
drawBoard();
display.display();
delay(2000);
goFrame = 0;
currentState = GAMEOVER_ANIM;
}
break;
case MENU:
drawMenu();
if (digitalRead(BTN_LEFT) == LOW) {
if (difficulty > 0) difficulty--;
delay(150);
}
if (digitalRead(BTN_RIGHT) == LOW) {
if (difficulty < 2) difficulty++;
delay(150);
}
if (digitalRead(BTN_ENTER) == LOW) {
totalMines = mineCounts[difficulty];
animRadius = 0;
currentState = START_ANIM;
while (digitalRead(BTN_ENTER) == LOW) {
delay(10);
}
}
if (digitalRead(BTN_BACK) == LOW) {
currentState = SCORES_ALL;
while (digitalRead(BTN_BACK) == LOW) { delay(10); } // Debouncing
}
break;
case START_ANIM:
drawOpeningAnimation();
animRadius += 4;
if (animRadius > 100) {
gameStartTime = millis();
currentState = PLAYING;
}
break;
case PLAYING:
handleInput();
if (currentState == PLAYING) {
drawBoard();
}
break;
case PAUSE:
drawPause();
if (digitalRead(BTN_ENTER) == LOW) {
chordingEnabled = !chordingEnabled;
while(digitalRead(BTN_ENTER) == LOW);
delay(50);
}
if (digitalRead(BTN_BACK) == LOW) {
currentState = PLAYING;
while(digitalRead(BTN_BACK) == LOW);
delay(50);
lastInputTime = millis();
}
break;
case WIN_ANIM:
drawWinAnimation();
winFrame++;
delay(10);
if (digitalRead(BTN_BACK) == LOW) {
currentState = MENU;
firstClick = true;
clearBoard();
cursorX = 0;
cursorY = 0;
delay(200);
}
if (digitalRead(BTN_ENTER) == LOW) {
currentState = SCORES_LOCAL;
while (digitalRead(BTN_ENTER) == LOW) { delay(10); } // Debouncing
}
break;
case SCORES_ALL:
drawScoresAll();
if (digitalRead(BTN_BACK) == LOW) {
currentState = MENU;
while (digitalRead(BTN_BACK) == LOW) { delay(10); }
}
break;
case SCORES_LOCAL:
drawScoresLocal();
if (digitalRead(BTN_BACK) == LOW) {
currentState = MENU; // Powrót od razu do czystego MENU
firstClick = true;
clearBoard();
cursorX = 0;
cursorY = 0;
while (digitalRead(BTN_BACK) == LOW) { delay(10); }
}
break;
}
}