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.
1. Płytka Arduino UNO lub kompatybilna
2. Shield SIC Game Console
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
/*
* 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]);
}
}