Indiana Jones The Forbidden Path

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

Wciel się w rolę legendarnego archeologa i przetestuj swoją pamięć w starożytnej świątyni. Twoim celem jest przejście przez labirynt pełen ukrytych pułapek, takich jak zapadnie czy zatrute strzały. Haczyk? Bezpieczna ścieżka pojawia się tylko na kilka sekund na początku każdego etapu. Jeden fałszywy krok oznacza koniec przygody, a dotarcie do celu nagradzane jest bezcennym skarbem. Czy zdołasz zapamiętać drogę do wolności?

Niezbędne elementy

1. Płytka Arduino UNO

2. Konsola SIC Game Control

3. Chęci do gry!

Opis projektu

Projekt „Indiana Jones: The Forbidden Path” to autorska gra oparta na mikrokontrolerze Arduino UNO, która przenosi mechanikę klasycznych gier typu Memory w świat interaktywnej przygody. Całość została zaprojektowana tak, aby wystawić na próbę koncentrację i pamięć gracza, który musi bezpiecznie przeprowadzić bohatera przez starożytną świątynię. Kluczowym elementem projektu jest system generowania tras – na początku każdego etapu urządzenie tworzy unikalną, losową ścieżkę. Jest ona wyświetlana użytkownikowi jedynie przez kilka krótkich sekund, co zmusza do pełnego skupienia, zanim bezpieczny układ pól całkowicie zniknie z ekranu.

Właściwa rozgrywka polega na poruszaniu się po niewidocznym labiryncie „na pamięć”. Każdy krok jest analizowany przez system, a gracz musi wykazać się dużą uważnością, ponieważ świątynia jest pełna pułapek. Wejście na błędne pole natychmiast aktywuje mechanizmy obronne, takie jak zapadnia prowadząca do spadku w otchłań lub lecąca w stronę bohatera zatruta strzała. Taki błąd kończy rozgrywkę, co podkreśla wyzwanie, przed jakim stoi użytkownik.

Projekt został podzielony na etapy o rosnącym poziomie trudności. W miarę postępów trasy stają się coraz dłuższe i bardziej kręte, co sprawia, że ich zapamiętanie wymaga coraz większego wysiłku. Nagrodą za pomyślne przejście każdego poziomu jest zdobycie legendarnego skarbu, co symbolizuje wygraną i pozwala przejść do kolejnego wyzwania.

Sterowanie:

Aby zapewnić pełną kontrolę nad bohaterem, projekt wykorzystuje zestaw dedykowanych przycisków:

  • Przyciski kierunkowe: Pozwalają na swobodne poruszanie się po niewidocznej, poprawnej trasie (przód, tył, lewo, prawo).
  • Przycisk A: Odpowiada za szybki start gry i wejście do świata przygody.
  • Przycisk B (Pochodnia): Jest to specjalna funkcja wspomagająca. Pozwala ona na użycie pochodni, co umożliwia ponowne podejrzenie kawałka poprawnego przejścia w momencie zawahania. Jest to jednak ułatwienie limitowane – gracz może go użyć tylko raz na całą grę.

Aby uruchomić ponownie grę należy wcisnąć przycisk reset lub załadować ponownie kod.

Zdjęcia
rozpoczęcie gry
przykładowa mapa1
przykładowa mapa2
podczas grania
sterowanie
kod programu
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

#define i2c_Address 0x3c 
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

// --- PINY ---
#define PIN_A      5  // Przycisk potwierdzający/startowy
#define PIN_B 4 // Przycisk umiejętności specjalnej
#define PIN_GORA   7
#define PIN_PRAWO  8
#define PIN_DOL    9
#define PIN_LEWO   10

Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Wymiary i pozycjonowanie na ekranie
const int COLS = 12;      
const int ROWS = 6;       
const int CELL_SIZE = 10; 
const int OFFSET_X = 4;   
const int OFFSET_Y = 2;   

bool maze[COLS][ROWS]; 
int playerX = 0, playerY = 0;
bool showingPath = false;
unsigned long previewStartTime;

int levelCounter = 1;

//umiejetnosc specjalna

int powerUses = 1;        // Limit: 1 użycie na całą grę
bool powerActive = false; // Czy umiejętność jest teraz włączona
unsigned long powerTimer; // Odliczanie 2 sekund


// Stany gry
enum GameState {
  TITLE_SCREEN,
  PLAYING,
  GAME_OVER
};

GameState currentState = TITLE_SCREEN;

// --- LOGIKA GENERATORA MAPY (Zoptymalizowana dla Arduino) ---

bool canPlacePath(int x, int y) {
  if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return false;
  if (maze[x][y]) return false; // Pole już zajęte

  int neighbors = 0;
  if (x + 1 < COLS && maze[x+1][y]) neighbors++;
  if (x - 1 >= 0   && maze[x-1][y]) neighbors++;
  if (y + 1 < ROWS && maze[x][y+1]) neighbors++;
  if (y - 1 >= 0   && maze[x][y-1]) neighbors++;

  // Ścieżka może dotykać tylko jednego kafelka (żeby nie tworzyć pętli)
  return (neighbors <= 1);
}

void generateMazeDFS(int startY) {
  // Czyszczenie mapy
  for(int x=0; x<COLS; x++) {
    for(int y=0; y<ROWS; y++) {
      maze[x][y] = false;
    }
  }

  // Używamy prostych tablic do zapamiętywania ścieżki (Backtracking)
  int historyX[72];
  int historyY[72];
  int step = 0;

  int curX = 0;
  int curY = startY;
  
  maze[curX][curY] = true;
  historyX[step] = curX;
  historyY[step] = curY;

  while (curX < COLS - 1) {
    int validDirs[4]; // 0: Prawo, 1: Dół, 2: Góra, 3: Lewo
    int count = 0;

    if (canPlacePath(curX + 1, curY)) validDirs[count++] = 0;
    if (canPlacePath(curX, curY + 1)) validDirs[count++] = 1;
    if (canPlacePath(curX, curY - 1)) validDirs[count++] = 2;
    if (canPlacePath(curX - 1, curY)) validDirs[count++] = 3;

    if (count > 0) {
      // Algorytm promuje ruch w prawo, żeby mapa szybciej parła do przodu
      int dir = validDirs[random(count)]; 
      if (random(50 + 50*levelCounter) < 90 && canPlacePath(curX + 1, curY)) {
        dir = 0; // Wymuś krok w prawo jeśli to możliwe (60% szans)                     // u nas 90% trudnosc
      }

      step++;
      if (dir == 0) curX++;
      else if (dir == 1) curY++;
      else if (dir == 2) curY--;
      else if (dir == 3) curX--;

      maze[curX][curY] = true;
      historyX[step] = curX;
      historyY[step] = curY;
    } else {
      // Zablokowaliśmy się - ślepy zaułek. Cofamy się 
      if (step > 0) {
        maze[curX][curY] = false; // Usuwamy złą ścieżkę
        step--;
        curX = historyX[step];
        curY = historyY[step];
      } else {
        // Zabezpieczenie na wypadek absolutnej blokady początkowej
        break; 
      }
    }
  }
}

// --- FUNKCJE RYSOWANIA I STANU GRY ---

void drawGrid() {
  for(int x=0; x<COLS; x++) {
    for(int y=0; y<ROWS; y++) {
      int dx = x * CELL_SIZE + OFFSET_X;
      int dy = y * CELL_SIZE + OFFSET_Y;
      display.drawRect(dx, dy, CELL_SIZE, CELL_SIZE, SH110X_WHITE);
      
      // 1. Standardowy podgląd na starcie poziomu
      if (showingPath && maze[x][y]) {
        display.fillRect(dx + 4, dy + 4, 2, 2, SH110X_WHITE);
      }

      // 2. NOWA MECHANIKA: Podświetlenie w promieniu 2
      if (powerActive && maze[x][y]) {
        // Obliczamy odległość w kwadracie (promień 2 komórki)
        if (abs(x - playerX) <= 2 && abs(y - playerY) <= 2) {
          // Rysujemy nieco większą kropkę (4x4), żeby odróżnić ją od podpowiedzi startowej
          display.fillRect(dx + 3, dy + 3, 4, 4, SH110X_WHITE);
        }
      }
    }
  }
}

void killProgram() {
  display.clearDisplay();
  
  if(levelCounter >= 4){
    display.setTextSize(2);
    display.setCursor(2, 10);
    display.print("Gratulacje");
    display.setTextSize(1);
    display.setCursor(10, 40);
    display.print("Odkryles zaginiony");
    display.setCursor(45, 50);
    display.print("skarb!");

 
                   // To tutja jest napis koncowy
  }
  else{
    display.setTextSize(2);
    display.setCursor(5, 10);
    display.print("Nie zyjesz");
    display.setTextSize(1);
    display.setCursor(20, 40);
    display.print("Gubisz sie w");
    display.setCursor(2, 50);
    display.print("czelusciach swiatyni!");

   
  }
  display.display();
  delay(2000);
  

    

  
  // Nieskończona pętla - całkowicie zamraża program
  while(true) {
    // Arduino "zawiesza się" tutaj
  }
}

void animacjaSpadania(int x, int y) {
    // Obliczamy środek komórki, w której stoi gracz
    int centerX = x * CELL_SIZE + OFFSET_X + (CELL_SIZE / 2);
    int centerY = y * CELL_SIZE + OFFSET_Y + (CELL_SIZE / 2);

    // Zmniejszamy rozmiar kropki od 6 do 0 pikseli
    for (int size = 6; size >= 0; size--) {
        display.clearDisplay();
        drawGrid(); // Rysujemy siatkę w tle, żeby było widać gdzie spadamy
        
        // Rysujemy kropkę scentrowaną, która się zmniejsza
        int halfSize = size / 2;
        display.fillRect(centerX - halfSize, centerY - halfSize, size, size, SH110X_WHITE);
        
        display.display();
        delay(100); // Prędkość spadania
    }
    delay(500); // Chwila ciemności po "upadku"
}

void animacjaStrzaly(int x, int y, bool ruchPionowy) {
  int targetX = x * CELL_SIZE + OFFSET_X + (CELL_SIZE / 2);
  int targetY = y * CELL_SIZE + OFFSET_Y + (CELL_SIZE / 2);

  if (ruchPionowy) {
    // Gracz ruszył się GÓRA/DÓŁ -> Strzała leci POZIOMO (z lewej do prawej)
    for (int ax = 0; ax <= targetX; ax += 5) {
      display.clearDisplay();
      drawGrid();
      // Gracz
      display.fillRect(x * CELL_SIZE + OFFSET_X + 2, y * CELL_SIZE + OFFSET_Y + 2, 6, 6, SH110X_WHITE);
      // Rysowanie strzały (linia + grot)
      display.drawLine(ax - 8, targetY, ax, targetY, SH110X_WHITE);
      display.drawTriangle(ax, targetY - 2, ax, targetY + 2, ax + 4, targetY, SH110X_WHITE);
      display.display();
    }
  } else {
    // Gracz ruszył się LEWO/PRAWO -> Strzała leci PIONOWO (z góry do dołu)
    for (int ay = 0; ay <= targetY; ay += 4) {
      display.clearDisplay();
      drawGrid();
      // Gracz
      display.fillRect(x * CELL_SIZE + OFFSET_X + 2, y * CELL_SIZE + OFFSET_Y + 2, 6, 6, SH110X_WHITE);
      // Rysowanie strzały (linia + grot)
      display.drawLine(targetX, ay - 8, targetX, ay, SH110X_WHITE);
      display.drawTriangle(targetX - 2, ay, targetX + 2, ay, targetX, ay + 4, SH110X_WHITE);
      display.display();
    }
  }
  
  // Efekt uderzenia - mignięcie ekranu
  display.invertDisplay(true);
  delay(100);
  display.invertDisplay(false);
  delay(500);
}


void setup() {
  pinMode(PIN_A,     INPUT_PULLUP);
  pinMode(PIN_B, INPUT_PULLUP);
  pinMode(PIN_GORA,  INPUT_PULLUP);
  pinMode(PIN_PRAWO, INPUT_PULLUP);
  pinMode(PIN_DOL,   INPUT_PULLUP);
  pinMode(PIN_LEWO,  INPUT_PULLUP);

  display.begin(i2c_Address, true);
  display.setRotation(2); 
  display.setTextColor(SH110X_WHITE);
  randomSeed(analogRead(0)); // Losowość na podstawie szumu na pinie analogowym

  // --- EKRAN STARTOWY ---
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(20, 10);
  display.print("INDIANA");
  display.setCursor(35, 28);
  display.print("JONES");
  display.setTextSize(1);
  display.setCursor(10, 50);
  display.print("the forbidden path");
  display.display();
  delay(2000); 
}



void loop() {

    
  if (currentState == TITLE_SCREEN) {
   
    display.clearDisplay();
    display.setTextSize(2);
    display.setCursor(25, 10);
    display.print("WITAJ!");
    display.setTextSize(1);
    display.setCursor(15, 40);
    display.print("Kliknij A aby");
    display.setCursor(35, 50);
    display.print("rozpoczac");
    display.display();

    // Czekamy na kliknięcie przycisku A
    if (digitalRead(PIN_A) == LOW) {
      playerY = random(0, ROWS); // Pierwszy start w losowym miejscu Y
      playerX = 0;
      generateMazeDFS(playerY);
      
      showingPath = true;
      previewStartTime = millis();
      currentState = PLAYING;
      delay(300); // Debouncing przycisku
    }
  } 
  
  else if (currentState == PLAYING) {
    // Wygaszanie podglądu ścieżki po 2 sekundach
    if (millis() - previewStartTime > 2000) showingPath = false;
    
    bool pionowo = false;
    
    bool moved = false;
    if (digitalRead(PIN_GORA) == LOW && playerY > 0) { playerY--; moved = true; pionowo = true; }
    else if (digitalRead(PIN_DOL) == LOW && playerY < ROWS - 1) { playerY++; moved = true; pionowo = true;}
    else if (digitalRead(PIN_LEWO) == LOW && playerX > 0) { playerX--; moved = true; pionowo = false; }
    else if (digitalRead(PIN_PRAWO) == LOW) { playerX++; moved = true; pionowo = false; }

    if (digitalRead(PIN_B) == LOW && powerUses > 0 && !powerActive) {
      powerActive = true;
      powerUses--; // Zużywamy jedyną szansę
      powerTimer = millis();
    }

    // Wyłączanie podświetlenia po 2 sekundach
    if (powerActive && (millis() - powerTimer > 2000)) {
      powerActive = false;
    }

    if (moved) {
      delay(200); // Czas pomiędzy krokami (debounce)

      if (playerX >= COLS) {
        // SUKCES: Przechodzimy na nową planszę
        levelCounter++;
        if(levelCounter >=4){
          currentState = GAME_OVER;
          killProgram();
        }

        int nextStartY = playerY; // Zachowujemy obecną pozycję Y!
        generateMazeDFS(nextStartY); // Budujemy ścieżkę zaczynając od tego Y
        playerX = 0; // Wracamy na lewą krawędź
        
        showingPath = true;
        previewStartTime = millis();
      } 
      else if (!maze[playerX][playerY]) {
        // Animacja spadania przed końcem gry
        int los = random(0,2);
        if(los == 0){
          animacjaSpadania(playerX, playerY);
        }
        else{
          animacjaStrzaly(playerX, playerY, pionowo);
        }

        // BŁĄD: Gracz zszedł z wyznaczonej trasy
        currentState = GAME_OVER;
        killProgram();
      }
    }

    display.clearDisplay();
    drawGrid();
    
    // Rysowanie gracza
    int px = playerX * CELL_SIZE + OFFSET_X + 2;
    int py = playerY * CELL_SIZE + OFFSET_Y + 2;
    display.fillRect(px, py, 6, 6, SH110X_WHITE);
    
    display.display();
  }
}
Pliki_projektu
Schemat
Youtube
Tagi
IndianaJones gra skarb labirynt wyzwania