#include #include #include #include #include // ===== 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; } }