LUMBERJACK - "TIMBERMAN" z twistem

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

Projekt implementuje grę zręcznościową „Lumberjack" na Arduino UNO z wyświetlaczem SH1106 128×64. Gracz wciela się w drwala stojącego po lewej stronie pnia drzewa i musi jak najszybciej ścinać kolejne bloki, unikając gałęzi. Każde ścięcie wydłuża odliczanie o sekundę – im szybciej gracz pracuje, tym więcej czasu zyskuje. Rozgrywkę kończy kolizja z gałęzią lub upływ licznika czasu.

Niezbędne elementy

1. Płytka Arduino UNO lub kompatybilna

2. Shield SIC Game Console

Opis projektu

Projekt implementuje grę zręcznościową „Lumberjack" inspirowaną popularną grą mobilną o tej samej nazwie. Celem gracza jest ścięcie jak największej liczby bloków pnia drzewa w ograniczonym czasie, unikając gałęzi wystających po jego stronie.

ELEMENTY PROGRAMU

1. Ekran startowy Program rozpoczyna się od zainicjowania pinów przycisków w trybie INPUT_PULLUP, inicjalizacji wyświetlacza SH1106 przez magistralę I2C oraz wyświetlenia ekranu tytułowego z nazwą gry „LUMBERJACK", miniaturą pnia i instrukcją sterowania.

2. Obsługa wyświetlacza Wyświetlanie stanu gry odbywa się z użyciem bibliotek Adafruit GFX i Adafruit SH110X. Każda klatka rysowana jest od zera: czyszczenie bufora, rysowanie paska statusu, bloków pnia z gałęziami, postaci gracza oraz paska odliczania czasu na dole ekranu. Wyświetlacz pracuje w trybie obróconym o 180°. Dostępna jest także funkcja diagnostyczna dumpScreen() eksportująca zawartość bufora przez Serial jako bitmapę PBM.

3. Mechanika gry Pień składa się z 7 widocznych bloków przechowywanych w buforze o rozmiarze 9. Każdy blok może być czysty (BLK_PLAIN) lub mieć gałąź skierowaną ku górze (BLK_TOP) albo ku dołowi (BLK_BOT). Po każdym ścięciu pień przesuwa się w lewo, a nowy losowy blok dołączany jest z prawej strony bufora. Gracz pozycjonuje się na górze lub dole pnia – kolizja z gałęzią po jego stronie kończy grę natychmiast.

4. System czasu Rozgrywka startuje z 5-sekundowym odliczaniem. Każde poprawne ścięcie nagradza gracza 200 ms dodatkowego czasu, co motywuje do utrzymywania wysokiego tempa. Aktualny stan licznika wyświetlany jest liczbowo w górnym pasku oraz wizualnie jako proporcjonalny pasek wypełnienia na dole ekranu.

5. Kontrola użytkownika Gracz używa dwóch par przycisków: PIN 7/10 przenosi postać na górę pnia i wykonuje ścięcie, PIN 9/8 przenosi na dół. PIN 4 wraca do menu, PIN 5 restartuje grę. Wszystkie przyciski obsługiwane są z debouncingiem 150 ms zapobiegającym wielokrotnemu rejestrowaniu jednego naciśnięcia.

6. Działanie ważnych funkcji

btnPressed() – obsługa pojedynczego przycisku z debouncingiem opartym na millis()

pressedUp() / pressedDown() – aliasy łączące obie pary przycisków dla każdego kierunku

newBlock() – losuje typ kolejnego bloku z gwarancją, że dwie gałęzie nie wystąpią bezpośrednio po sobie

doChop() – sprawdza kolizję z gałęzią aktywnego bloku (tree[0]), inkrementuje wynik, przesuwa bufor pnia i przedłuża deadlineMs o TIME_BONUS

drawBlock() – rysuje pojedynczy blok pnia wraz z opcjonalną gałęzią (górną lub dolną) wyrastającą pionowo z bloku

drawPlayer() – rysuje pikselową postać drwala w dwóch pozycjach (góra/dół) z animacją uderzenia siekierą skierowaną w prawo, ku pniowi

drawGame() – renderuje pełną klatkę gry: pasek statusu, pień, postać i pasek czasu

dumpScreen() – eksportuje bitmapę ekranu przez Serial w formacie PBM (P4) do celów debugowania i podglądu na komputerze

Zdjęcia
kod programu

/*
 * TIMBERMAN dla Arduino Uno + wyświetlacz SH110x 128x64 (I2C)
 *
 * Podłączenie wyświetlacza I2C:
 *   SDA → A4  |  SCL → A5  |  VCC → 3.3V/5V  |  GND → GND
 *
 * Układ przycisków (INPUT_PULLUP, aktywne LOW):
 *
 *          [7]              [4]
 *      [10]    [8]
 *          [9]              [5]
 *
 *   PIN 7  - GÓRA    (gracz przenosi się na górę pnia i ścina)
 *   PIN 9  - DÓŁ     (gracz przenosi się na dół pnia i ścina)
 *   PIN 10 - LEWO     też GÓRA)
 *   PIN 8  - PRAWO    też DÓŁ)
 *   PIN 4  - START / powrót do menu
 *   PIN 5  - RESTART
 *
 * Mechanika:
 *   - Gracz stoi PO LEWEJ stronie pnia, zwrócony w prawo
 *   - Aktywny blok (ten, który gracz ścina) = tree[0] – najbliższy gracza
 *   - Po ścięciu pień przesuwa się w lewo, nowy blok dochodzi z prawej
 *   - Gałęzie wyrastają od każdego bloku w LEWO (w stronę gracza)
 *   - Kolizja = gałąź po stronie gracza → GAME OVER
 *   - Czas startowy 60 s; każde ścięcie dodaje +1 s
 *   - Pasek odliczania na dole ekranu
 */

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

// ─── Wyświetlacz ─────────────────────────────────────────────────────────────
#define SCREEN_W  128
#define SCREEN_H   64
#define I2C_ADDR  0x3C    // zmień na 0x3D jeśli ekran nie działa
Adafruit_SH1106G display(SCREEN_W, SCREEN_H, &Wire, -1);

// ─── Piny przycisków ─────────────────────────────────────────────────────────
#define BTN_UP     7
#define BTN_DOWN   9
#define BTN_LEFT  10
#define BTN_RIGHT  8
#define BTN_A      4
#define BTN_B      5

#define NUM_BTNS   6
const uint8_t BTNS[NUM_BTNS] = {BTN_UP, BTN_DOWN, BTN_LEFT, BTN_RIGHT, BTN_A, BTN_B};
unsigned long lastBtn[NUM_BTNS] = {};
#define DEBOUNCE 150UL

bool btnPressed(uint8_t pin) {
  for (uint8_t i = 0; i < NUM_BTNS; i++) {
    if (BTNS[i] == pin) {
      if (digitalRead(pin) == LOW && millis() - lastBtn[i] > DEBOUNCE) {
        lastBtn[i] = millis();
        return true;
      }
      return false;
    }
  }
  return false;
}

bool pressedUp()    { return btnPressed(BTN_UP)   || btnPressed(BTN_LEFT);  }
bool pressedDown()  { return btnPressed(BTN_DOWN)  || btnPressed(BTN_RIGHT); }
bool pressedStart() { return btnPressed(BTN_A)     || btnPressed(BTN_B);     }

// ─── Wymiary ─────────────────────────────────────────────────────────────────
#define BLOCK_W     14
#define BLOCK_H     14
#define BRANCH_LEN  10
#define BRANCH_H     4
#define NUM_BLOCKS   7

#define TRUNK_Y     ((SCREEN_H - BLOCK_H) / 2)   // 25

// TRUNK_X0=16 → gałąź sięga do x=6, kółko środek x=3 – mieści się na ekranie
#define TRUNK_X0    16

#define PLAYER_W    9
#define PLAYER_H    12
#define PLAYER_X    (TRUNK_X0 - PLAYER_W - 2)    // 5 – gracz PO LEWEJ
#define PLAYER_Y_TOP (TRUNK_Y - PLAYER_H)
#define PLAYER_Y_BOT (TRUNK_Y + BLOCK_H)

// ─── Typy bloków ─────────────────────────────────────────────────────────────
#define BLK_PLAIN  0
#define BLK_TOP    1   // gałąź góra → niebezpieczna dla gracza NA GÓRZE
#define BLK_BOT    2   // gałąź dół  → niebezpieczna dla gracza NA DOLE

// ─── Odliczanie ──────────────────────────────────────────────────────────────
#define TIME_START  5000UL   // 5 sekund na start
#define TIME_BONUS   200UL   // +0,2 s za każde ścięcie

// ─── Stan gry ────────────────────────────────────────────────────────────────
enum State { MENU, PLAYING, GAMEOVER };
State gameState = MENU;
#define BUF_SZ (NUM_BLOCKS + 2)
uint8_t  tree[BUF_SZ];
uint8_t  playerPos  = 0;   // 0=góra, 1=dół
uint16_t score      = 0;
uint16_t hiScore    = 0;
uint32_t startMs    = 0;
uint32_t deadlineMs = 0;   // moment końca czasu (ms)
uint16_t elapsed    = 0;   // czas gry w sekundach (do ekranu GAME OVER)

bool    chopAnim  = false;
uint8_t chopTimer = 0;
#define CHOP_DUR 3

// ─── Generowanie bloków ───────────────────────────────────────────────────────
uint8_t newBlock(uint8_t prev) {
  uint8_t r;
  do { r = (uint8_t)random(3); } while (r != BLK_PLAIN && prev != BLK_PLAIN);
  return r;
}

// ─── Setup ───────────────────────────────────────────────────────────────────
void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < NUM_BTNS; i++) pinMode(BTNS[i], INPUT_PULLUP);
  display.begin(I2C_ADDR, true);
  display.setRotation(2);   // obrót 180°
  display.clearDisplay();
  display.display();
  randomSeed(analogRead(A0));
  drawMenu();
}

// ─── Loop ────────────────────────────────────────────────────────────────────
void loop() {
  switch (gameState) {

    case MENU:
      if (pressedUp() || pressedDown() || pressedStart()) startGame();
      break;

    case PLAYING: {
      if (btnPressed(BTN_B)) {
  dumpScreen();
}
      bool up   = pressedUp();
      bool down = pressedDown();
      if (up || down) {
        playerPos = down ? 1 : 0;
        doChop();
        if (gameState != PLAYING) break;
      }
      if (btnPressed(BTN_A)) { drawMenu(); break; }

      // ── Sprawdź koniec czasu ──────────────────────────────────────────────
      if (millis() >= deadlineMs) {
        elapsed = (uint16_t)((millis() - startMs) / 1000UL);
        if (score > hiScore) hiScore = score;
        gameState = GAMEOVER;
        drawGameOver();
        break;
      }

      if (chopAnim) {
        chopTimer++;
        if (chopTimer >= CHOP_DUR) { chopAnim = false; chopTimer = 0; }
        delay(35);
      }
      drawGame();
      break;
    }

    case GAMEOVER:
      if (pressedStart() || pressedUp() || pressedDown()) startGame();
      break;
  }
}

// ─── Start gry ───────────────────────────────────────────────────────────────
void startGame() {
  score = 0; playerPos = 0; chopAnim = false; chopTimer = 0;
  // Pierwsze trzy bloki czyste – bezpieczny start
  tree[0] = BLK_PLAIN; tree[1] = BLK_PLAIN; tree[2] = BLK_PLAIN;
  for (uint8_t i = 3; i < BUF_SZ; i++) tree[i] = newBlock(tree[i-1]);
  startMs    = millis();
  deadlineMs = startMs + TIME_START;
  gameState  = PLAYING;
  drawGame();
}

// ─── Cięcie ──────────────────────────────────────────────────────────────────
void doChop() {
  // Gracz po LEWEJ – aktywny blok to tree[0] (lewoskrajny, najbliższy gracza)
  uint8_t active = tree[0];
  bool hit = (playerPos == 0 && active == BLK_TOP) ||
             (playerPos == 1 && active == BLK_BOT);
  if (hit) {
    elapsed = (uint16_t)((millis() - startMs) / 1000UL);
    if (score > hiScore) hiScore = score;
    gameState = GAMEOVER;
    drawGameOver();
    return;
  }
  score++;
  deadlineMs += TIME_BONUS;   // +1 s nagrody za ścięcie
  chopAnim = true; chopTimer = 0;
  elapsed  = (uint16_t)((millis() - startMs) / 1000UL);
  // Przesuń pień w lewo, nowy blok wchodzi z prawej (bufor)
  for (uint8_t i = 0; i < BUF_SZ - 1; i++) tree[i] = tree[i+1];
  tree[BUF_SZ-1] = newBlock(tree[BUF_SZ-2]);
}

// ════════════════════════════════════════════════════════════════════════════
//  RYSOWANIE
// ════════════════════════════════════════════════════════════════════════════

void drawBlock(int x, int y, uint8_t type) {
  //Pien 
  display.fillRect(x, y, BLOCK_W, BLOCK_H, SH110X_WHITE);
  display.drawLine(x+2, y+4, x+BLOCK_W-3, y+4, SH110X_BLACK);
  display.drawLine(x+2, y+9, x+BLOCK_W-3, y+9, SH110X_BLACK);
  display.drawLine(x, y, x, y+BLOCK_H-1, SH110X_BLACK);

  // Gałęzie wyrastają W GÓRĘ (w stronę gracza)
 if (type == BLK_TOP) {
    int gx = x - BRANCH_LEN + 10, gy = y - BRANCH_H;
    display.fillRect(gx, gy, BRANCH_H, -BRANCH_LEN, SH110X_WHITE);
    display.drawLine(gx, gy+BRANCH_H-1, x-1, gy+BRANCH_H-1, SH110X_BLACK);
    display.fillCircle(gx+2, gy+1, 3, SH110X_WHITE);
    display.drawPixel(gx+2, gy+1, SH110X_BLACK);
  }
  // Gałęzie wyrasatją w DÓŁ 
  if (type == BLK_BOT) {
    int gx = x - BRANCH_LEN + 10, gy = y + BLOCK_H;
    display.fillRect(gx, gy, BRANCH_H, BRANCH_LEN + 5, SH110X_WHITE);
    display.drawLine(gx, gy, x-1, gy, SH110X_BLACK);
    display.fillCircle(gx+2, gy+2, 3, SH110X_WHITE);
    display.drawPixel(gx+2, gy+2, SH110X_BLACK);
  }
}

// Gracz zwrócony W PRAWO (ręka/siekiera po prawej stronie, w stronę pnia)
void drawPlayer(int x, int y, uint8_t pos, bool chop) {
  if (pos == 0) {
    // ── Gracz na GÓRZE pnia ──────────────────────────────────────────────────
    display.fillRect(x+1, y,    7, 1, SH110X_WHITE); // kapelusz – rondo
    display.fillRect(x+2, y+1,  5, 2, SH110X_WHITE); // kapelusz – główka
    display.fillRect(x+2, y+3,  5, 3, SH110X_WHITE); // głowa
    display.fillRect(x+1, y+6,  7, 4, SH110X_WHITE); // ciało
    display.drawLine(x+4, y+6, x+4, y+9, SH110X_BLACK);  // pionowa linia paska
    display.drawLine(x+1, y+8, x+7, y+8, SH110X_BLACK);  // pozioma linia paska
    display.fillRect(x+1, y+10, 2, 2, SH110X_WHITE); // noga L
    display.fillRect(x+5, y+10, 2, 2, SH110X_WHITE); // noga P
    // Ręka/siekiera po PRAWEJ (→ pień)
    if (chop) {
      display.fillRect(x+PLAYER_W,   y+4, 5, 3, SH110X_WHITE); // ostrze
      display.drawLine(x+PLAYER_W-1, y+6, x+PLAYER_W+3, y+8, SH110X_WHITE);
    } else {
      display.fillRect(x+PLAYER_W, y+6, 2, 4, SH110X_WHITE);   // ramię w spocz.
    }
  } else {
    // ── Gracz na DOLE pnia (odwrócony) ──────────────────────────────────────
    display.fillRect(x+1, y,    2, 2, SH110X_WHITE); // noga L
    display.fillRect(x+5, y,    2, 2, SH110X_WHITE); // noga P
    display.fillRect(x+1, y+2,  7, 4, SH110X_WHITE); // ciało
    display.drawLine(x+4, y+2, x+4, y+5, SH110X_BLACK);
    display.drawLine(x+1, y+4, x+7, y+4, SH110X_BLACK);
    display.fillRect(x+2, y+6,  5, 3, SH110X_WHITE); // głowa
    display.fillRect(x+2, y+9,  5, 2, SH110X_WHITE); // kapelusz – główka
    display.fillRect(x+1, y+11, 7, 1, SH110X_WHITE); // kapelusz – rondo
    // Ręka/siekiera po PRAWEJ (→ pień)
    if (chop) {
      display.fillRect(x+PLAYER_W,   y+4, 5, 3, SH110X_WHITE);
      display.drawLine(x+PLAYER_W-1, y+5, x+PLAYER_W+3, y+3, SH110X_WHITE);
    } else {
      display.fillRect(x+PLAYER_W, y+4, 2, 4, SH110X_WHITE);
    }
  }
}

void drawGame() {
  display.clearDisplay();

  // ── Czas pozostały ──
  uint32_t now  = millis();
  uint32_t left = (now < deadlineMs) ? (deadlineMs - now) : 0;
  uint16_t leftSec = (uint16_t)(left / 1000UL);

  // ── Pasek górny ──
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);
  display.print(F("Wynik:"));
  display.print(score);
  display.setCursor(88, 0);
  display.print(leftSec);
  display.print(F("s"));
  display.drawLine(0, 8, SCREEN_W, 8, SH110X_WHITE);

  // ── Pień ──
  for (uint8_t i = 0; i < NUM_BLOCKS; i++)
    drawBlock(TRUNK_X0 + i * BLOCK_W, TRUNK_Y, tree[i]);

  // ── Gracz (lewa strona) ──
  int py = (playerPos == 0) ? PLAYER_Y_TOP : PLAYER_Y_BOT;
  drawPlayer(PLAYER_X, py, playerPos, chopAnim && chopTimer < 2);

  // ── Pasek odliczania (dół) ──
  // Szerokość proporcjonalna do pozostałego czasu; ref = TIME_START
  uint32_t bw32 = left * (uint32_t)SCREEN_W / TIME_START;
  uint8_t  barW = (bw32 >= SCREEN_W) ? (uint8_t)SCREEN_W : (uint8_t)bw32;
  display.fillRect(0, SCREEN_H-5, barW, 5, SH110X_WHITE);  // wypełnienie
  display.drawRect(0, SCREEN_H-5, SCREEN_W, 5, SH110X_WHITE); // ramka (zawsze pełna)

  display.display();
}

void drawMenu() {
  gameState = MENU;
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 1);   // ← przesunięte o 1 znak w lewo (było 4)
  display.print(F("LUMBERJACK"));
  display.drawLine(0, 18, SCREEN_W, 18, SH110X_WHITE);

  // Miniatura pnia
  for (uint8_t i = 0; i < 5; i++) {
    int bx = 4 + i * (BLOCK_W+1);
    display.fillRect(bx, 26, BLOCK_W, BLOCK_H, SH110X_WHITE);
    display.drawLine(bx+2, 30, bx+BLOCK_W-3, 30, SH110X_BLACK);
  }
  // Przykładowa gałąź
  display.fillRect(4 + 2*(BLOCK_W+1) - BRANCH_LEN, 22, BRANCH_LEN, BRANCH_H, SH110X_WHITE);
  display.fillCircle(4 + 2*(BLOCK_W+1) - BRANCH_LEN - 3, 24, 3, SH110X_WHITE);

  display.setTextSize(1);
  display.setCursor(4, 46);
  display.print(F("Gora i Dol!"));
  display.setCursor(4, 56);
  display.print(F("Wcisnij aby grac!"));
  display.display();
}

void drawGameOver() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(8, 1);
  display.print(F("GAME OVER"));
  display.drawLine(0, 18, SCREEN_W, 18, SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(4, 22);
  display.print(F("Scieto : ")); display.print(score); display.print(F(" blokow"));
  display.setCursor(4, 32);
  display.print(F("Czas   : ")); display.print(elapsed); display.print(F(" sek"));
  if (elapsed > 0) {
    display.setCursor(4, 42);
    display.print(F("Tempo  : "));
    uint16_t r10 = (uint32_t)score * 10 / elapsed;
    display.print(r10/10); display.print('.'); display.print(r10%10);
    display.print(F(" bl/s"));
  }
  display.setCursor(4, 52);
  display.print(F("Rekord : ")); display.print(hiScore);
  display.drawLine(0, 62, SCREEN_W, 62, SH110X_WHITE);
  display.display();
}  // put your main code here, to run repeatedly:


void dumpScreen() {
  uint8_t *buf = display.getBuffer();

  Serial.println(F("P4"));
  Serial.println(F("128 64"));

  for (int i = 0; i < 1024; i++) {
    Serial.write(buf[i]);
  }
}

Pliki_projektu
Youtube
Tagi
timberman arduino gra konsola zręcznościowa