Glass Maze

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

„Glass Maze” to minimalistyczny survival-horror zaprojektowany z myślą o mikrokontrolerach i małych ekranach OLED. Gracz trafia do proceduralnie generowanego, całkowicie pogrążonego w ciemności labiryntu, gdzie celem jest odnalezienie wyjścia i przejście na kolejny poziom.

Kluczową mechaniką jest latarka o ograniczonym zasięgu i baterii – oświetla jedynie wąski stożek przed postacią, zmuszając do ostrożnego i strategicznego korzystania z światła. Im mniej energii, tym słabsza widoczność, a jej całkowite wyczerpanie oznacza natychmiastową porażkę.

Każdy ukończony poziom odnawia część baterii, ale z czasem nagroda maleje, zwiększając trudność. Dodatkowe zagrożenie stanowi potwór, który pojawia się po pewnym czasie i ściga gracza, stając się coraz szybszy wraz z postępem. Choć ukrywa się w mroku, co kilka sekund zdradza swoją pozycję krótkim błyskiem.

Gra buduje napięcie poprzez ograniczoną widoczność, presję czasu i nieustanne zagrożenie, gdzie każdy błąd może zakończyć się śmiertelnym spotkaniem.

Niezbędne elementy

 

1. Płytka Arduino UNO

2. ekran oled 128x64

Opis projektu

„Glass Maze” to trzymająca w napięciu gra survival-horror o minimalistycznej oprawie wizualnej, stworzona z myślą o mikrokontrolerach z niewielkim ekranem OLED, w której wcielasz się w postać uwięzioną w proceduralnie generowanym, całkowicie mrocznym labiryncie i musisz znaleźć wyjście, by przejść do kolejnego etapu. Najważniejszą mechaniką jest system pola widzenia oparty na latarce, która rozświetla mrok jedynie w wąskim stożku przed bohaterem, czyniąc resztę planszy niewidoczną, co w połączeniu z nieustannym zużyciem ograniczonej baterii – gdzie jej poziom naładowania bezpośrednio wpływa na zasięg światła – zmusza gracza do strategicznego zarządzania zasilaniem i włączania latarki tylko w ostateczności. Całkowite wyczerpanie zasilania oznacza natychmiastowy koniec gry, a choć dotarcie do wyjścia generuje nowy układ ścian i odnawia część baterii, z każdym zdanym poziomem nagroda ta maleje, podnosząc stopień trudności. Dodatkowe zagrożenie stanowi przerażający potwór, pojawiający się na planszy po upływie określonego czasu, który inteligentnie podąża śladem gracza i przyspiesza z każdym pokonanym etapem, bezlitośnie skracając czas na znalezienie celu. Wróg ten ukrywa się w ciemnościach, ale gra regularnie, co 3 sekundy, zdradza jego pozycję poprzez ułamkowe mignięcie, co potęguje klaustrofobiczny klimat i sprawia, że każdy fałszywy krok lub pusta bateria prowadzą do nieuchronnego, śmiertelnego spotkania z bestią.

Zdjęcia
kod programu
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h> 

// ==========================================
// --- HARDWARE & DISPLAY CONFIGURATION ---
// ==========================================
#define i2c_Address 0x3c 
#define SCREEN_WIDTH 128 
#define SCREEN_HEIGHT 64 
#define OLED_RESET -1   

Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Button Pin Definitions (Connect to Ground, using INPUT_PULLUP)
#define BTN_UP    7  
#define BTN_DOWN  9  
#define BTN_LEFT  10  
#define BTN_RIGHT 8 
#define BTN_X     4  
#define BTN_Y     5  

// ==========================================
// --- TITLE SCREEN BITMAP (128x64) ---
// ==========================================
const unsigned char epd_bitmap_obraz [] PROGMEM = {
	0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 
	0xbf, 0xff, 0xff, 0xfe, 0x09, 0xf8, 0xf0, 0xe1, 0xff, 0x33, 0x0c, 0x00, 0x1f, 0xff, 0xff, 0xfd, 
	0xff, 0xff, 0xff, 0xfc, 0xf9, 0xf2, 0x66, 0x4c, 0xff, 0x33, 0x27, 0xe1, 0xff, 0xff, 0xff, 0xfd, 
	0xdf, 0xff, 0xff, 0xf9, 0xf9, 0xf3, 0x27, 0xcf, 0xff, 0x02, 0x67, 0x89, 0xff, 0xff, 0xff, 0xfb, 
	0xff, 0xff, 0xff, 0xf9, 0x09, 0xf3, 0x30, 0xe1, 0xff, 0x02, 0x67, 0x18, 0x3f, 0xff, 0xff, 0xfb, 
	0xef, 0xff, 0xff, 0xf9, 0x89, 0xf0, 0x3e, 0x7c, 0xff, 0x32, 0x06, 0x79, 0xff, 0xff, 0xff, 0xff, 
	0xff, 0xff, 0xff, 0xfc, 0x89, 0xf3, 0x26, 0x4c, 0xff, 0x32, 0x64, 0xf9, 0xff, 0xff, 0xff, 0xf7, 
	0xf7, 0xff, 0xff, 0xfe, 0x08, 0x13, 0x30, 0xe1, 0xff, 0x32, 0x64, 0x00, 0x1f, 0xff, 0xff, 0xff, 
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 
	0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xde, 
	0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3e, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x67, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 
	0xfc, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x07, 0x7c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0xfd, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7d, 
	0xfd, 0x7f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x01, 0x7f, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 
	0xfd, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x01, 0x7f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xfe, 0xfd, 
	0xfd, 0xbf, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xfd, 
	0xfd, 0xbf, 0xff, 0xff, 0xff, 0x90, 0x00, 0x00, 0x7f, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xfd, 0xff, 
	0xfd, 0xdf, 0xff, 0xff, 0xfc, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 
	0xfd, 0xff, 0xff, 0xff, 0xef, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xfb, 0xf9, 
	0xfd, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x0c, 0x00, 0x7f, 0xff, 0xff, 0xef, 0x3f, 0xff, 0xff, 0xf9, 
	0xfd, 0xef, 0xff, 0xf3, 0x7f, 0xf0, 0x0f, 0x04, 0x7f, 0xff, 0xff, 0xdf, 0xe7, 0xff, 0xf7, 0xfb, 
	0xfd, 0xff, 0xff, 0x97, 0xff, 0xf0, 0x07, 0x1c, 0x7f, 0xff, 0xff, 0xaf, 0xbc, 0xff, 0xf7, 0xff, 
	0xfd, 0xf7, 0xfd, 0xf7, 0xff, 0xf8, 0x00, 0x19, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xef, 0xff, 
	0xfd, 0xff, 0xef, 0xf7, 0xff, 0xf8, 0x00, 0x01, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xef, 0xf7, 
	0xfd, 0xff, 0xff, 0xf7, 0xff, 0xfc, 0x1f, 0x83, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xfe, 
	0xfd, 0xff, 0xff, 0xf7, 0xff, 0xf8, 0x07, 0xc7, 0x7f, 0xff, 0xff, 0xff, 0xbf, 0xff, 0xef, 0xfe, 
	0xfd, 0xff, 0xff, 0xf5, 0xff, 0xf0, 0x00, 0x0f, 0x7f, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xef, 0xff, 
	0xfd, 0xff, 0xff, 0xf7, 0xff, 0xc0, 0x00, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 
	0xfd, 0xff, 0xff, 0xf5, 0xff, 0x04, 0x61, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 
	0xfd, 0xff, 0xff, 0xf5, 0xfc, 0x0c, 0x70, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 
	0xfd, 0xff, 0xff, 0xf5, 0xf0, 0x38, 0xf8, 0x7f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 
	0xfd, 0xff, 0xff, 0xf5, 0xe0, 0xf8, 0xfc, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 
	0xfd, 0xff, 0xef, 0xf7, 0x83, 0xf8, 0xfe, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xef, 0xff, 
	0xfd, 0xf7, 0xfd, 0xf7, 0x0f, 0xf1, 0xfe, 0x3f, 0x79, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 
	0xfd, 0xf0, 0x00, 0x37, 0x1f, 0xf1, 0xff, 0x3f, 0x76, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x07, 0xff, 
	0xfd, 0xef, 0xff, 0xe7, 0x1f, 0xf1, 0xff, 0x17, 0x6f, 0x7f, 0xff, 0xff, 0xf3, 0xff, 0xf7, 0xff, 
	0xfd, 0xef, 0xff, 0xfc, 0x0f, 0xf3, 0xff, 0x03, 0x6f, 0x7f, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 
	0xfd, 0xe0, 0xff, 0xfe, 0x03, 0xf3, 0xfc, 0x07, 0x6f, 0x7f, 0xff, 0xfc, 0xff, 0xfc, 0x03, 0xff, 
	0x7d, 0xdf, 0xff, 0xfe, 0x00, 0xe3, 0xe0, 0x07, 0x66, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 
	0x7d, 0xff, 0xff, 0xfe, 0x00, 0x23, 0x80, 0x8f, 0x78, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xfd, 0xff, 
	0xfd, 0xbf, 0xff, 0xfc, 0x44, 0x00, 0x07, 0x8f, 0x70, 0x1f, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0xbd, 0xff, 0xff, 0xfc, 0x63, 0x00, 0x3f, 0x1f, 0x71, 0xdf, 0xef, 0xff, 0xff, 0xff, 0xfe, 0xff, 
	0xfd, 0x7f, 0xff, 0xfc, 0x61, 0xc0, 0xfe, 0x3f, 0x69, 0xaf, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0xfd, 0xff, 0xff, 0xff, 0xf0, 0x77, 0xde, 0x3f, 0x60, 0x01, 0xf1, 0xff, 0xff, 0xff, 0xff, 0x7f, 
	0xdc, 0xff, 0xff, 0xff, 0x81, 0xff, 0xf0, 0x7f, 0x6d, 0x07, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 
	0xfc, 0xff, 0xff, 0xf3, 0xf8, 0xff, 0xf8, 0x7f, 0x7c, 0x17, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xbf, 
	0xad, 0xff, 0xfc, 0x7f, 0xf8, 0xff, 0xf8, 0xcf, 0x78, 0x13, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xbf, 
	0xfd, 0xff, 0x8f, 0xff, 0xfc, 0x7f, 0xf0, 0x79, 0x4c, 0x99, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 
	0x7f, 0xe3, 0xff, 0xff, 0xf8, 0x7f, 0xf0, 0x1f, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xdf, 
	0xf8, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xfc, 0x1f, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 
	0xff, 0xff, 0xff, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 
	0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 
	0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 
	0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 
	0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 
	0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
	0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// ==========================================
// --- GAME SETTINGS & VARIABLES ---
// ==========================================

// Maze Settings
#define COLS 8
#define ROWS 4
#define CELL_SIZE 16

#define WALL_TOP 1
#define WALL_RIGHT 2
#define WALL_BOTTOM 4
#define WALL_LEFT 8
#define VISITED 16

byte maze[COLS][ROWS];

// State Machine
enum GameState { MENU, PLAYING, GAMEOVER };
GameState gameState = MENU;

// Player Settings
float playerX, playerY, playerAngle;
float moveSpeed = 1.5; 
float turnSpeed = 0.15; 
const float playerRadius = 3.0; 

// Flashlight & Battery
bool flashlightOn = false;
bool lastBtnXState = HIGH; 
float maxBattery = 45.0;
float currentBattery = maxBattery;
unsigned long lastDrainTime = 0;

// Game Stats & Menu Timing
int score = 0;
bool showStatus = false;
bool lastBtnYState = HIGH;
unsigned long blinkingInterval = 500; // Text is visible for 500ms, then invisible

// Enemy Settings
float enemyX, enemyY;
int enemyTargetCX, enemyTargetCY;
int enemyLastCX, enemyLastCY;
bool enemyActive = false;
unsigned long levelStartTime = 0;

// Player Sprite Definition ('i' shape to show direction)
const float iSprite[10][2] = {
  { 3,  0}, { 3, -1}, 
  { 0,  0}, { 0, -1}, 
  {-1,  0}, {-1, -1}, 
  {-2,  0}, {-2, -1}, 
  {-3,  0}, {-3, -1}  
};

// ==========================================
// --- SETUP & MAIN LOOP ---
// ==========================================

void setup() {
  Serial.begin(115200);
  
  pinMode(BTN_UP, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);
  pinMode(BTN_LEFT, INPUT_PULLUP);
  pinMode(BTN_RIGHT, INPUT_PULLUP);
  pinMode(BTN_X, INPUT_PULLUP);
  pinMode(BTN_Y, INPUT_PULLUP);

  randomSeed(analogRead(A0)); 
  delay(250); // Give OLED time to boot
  
  if (!display.begin(i2c_Address, true)) {
    Serial.println(F("SH1106 init failed"));
    while (true); 
  }

  display.setRotation(2); 
}

void loop() {
  switch (gameState) {
    case MENU:
      drawMenu(); // Draw static background elements
      
      // Add blinking text separately after graphics are in buffer
      if ((millis() / blinkingInterval) % 2 == 0) {
        display.setTextSize(1);
        
        // Flipped colors! White text on a Black highlight block
        display.setTextColor(SH110X_WHITE, SH110X_BLACK);
        display.setCursor(16, 54); 
        display.print(F("Press Y to Start"));
      }
      
      display.display(); // Push everything to the screen at once
      handleMenuInput();
      break;
      
    case PLAYING:
      handleInput();
      if (!showStatus) {
        updateGame();
        updateEnemy();
      }
      drawGame(); // drawGame handles its own display.display()
      break;

    case GAMEOVER:
      drawGameOver();
      handleGameOverInput();
      break;
  }
}

// ==========================================
// --- MENU (GLASS MAZE TITLE SCREEN) ---
// ==========================================

void drawMenu() {
  display.clearDisplay(); 
  
  // Render the custom bitmap image in BLACK (0s) over a WHITE background (1s) to invert it
  display.drawBitmap(0, 0, epd_bitmap_obraz, 128, 64, SH110X_BLACK, SH110X_WHITE);
}

void handleMenuInput() {
  if (digitalRead(BTN_Y) == LOW) {
    score = 0;
    currentBattery = maxBattery;
    enemyActive = false;
    generateMaze();
    
    playerX = CELL_SIZE / 2.0;
    playerY = SCREEN_HEIGHT - (CELL_SIZE / 2.0);
    playerAngle = -PI / 2.0; 
    
    levelStartTime = millis();
    gameState = PLAYING; 
    delay(200); 
  }
}

// ==========================================
// --- CORE GAMEPLAY LOGIC ---
// ==========================================

void generateMaze() {
  for (int x = 0; x < COLS; x++) {
    for (int y = 0; y < ROWS; y++) {
      maze[x][y] = WALL_TOP | WALL_RIGHT | WALL_BOTTOM | WALL_LEFT;
    }
  }

  int stackX[COLS * ROWS];
  int stackY[COLS * ROWS];
  int stackSize = 0;

  int cx = 0, cy = ROWS - 1; 
  maze[cx][cy] |= VISITED;
  stackX[stackSize] = cx;
  stackY[stackSize] = cy;
  stackSize++;

  while (stackSize > 0) {
    cx = stackX[stackSize - 1];
    cy = stackY[stackSize - 1];

    int neighbors[4];
    int numNeighbors = 0;

    if (cy > 0 && !(maze[cx][cy - 1] & VISITED)) neighbors[numNeighbors++] = 0; 
    if (cx < COLS - 1 && !(maze[cx + 1][cy] & VISITED)) neighbors[numNeighbors++] = 1; 
    if (cy < ROWS - 1 && !(maze[cx][cy + 1] & VISITED)) neighbors[numNeighbors++] = 2; 
    if (cx > 0 && !(maze[cx - 1][cy] & VISITED)) neighbors[numNeighbors++] = 3; 

    if (numNeighbors > 0) {
      int dir = neighbors[random(numNeighbors)];
      int nx = cx, ny = cy;

      if (dir == 0) { ny--; maze[cx][cy] &= ~WALL_TOP; maze[nx][ny] &= ~WALL_BOTTOM; }
      else if (dir == 1) { nx++; maze[cx][cy] &= ~WALL_RIGHT; maze[nx][ny] &= ~WALL_LEFT; }
      else if (dir == 2) { ny++; maze[cx][cy] &= ~WALL_BOTTOM; maze[nx][ny] &= ~WALL_TOP; }
      else if (dir == 3) { nx--; maze[cx][cy] &= ~WALL_LEFT; maze[nx][ny] &= ~WALL_RIGHT; }

      maze[nx][ny] |= VISITED;
      stackX[stackSize] = nx;
      stackY[stackSize] = ny;
      stackSize++;
    } else {
      stackSize--;
    }
  }
}

bool checkCollision(float x, float y) {
  if (x < playerRadius || x >= SCREEN_WIDTH - playerRadius) return true;
  if (y < playerRadius || y >= SCREEN_HEIGHT - playerRadius) return true;

  int cx = x / CELL_SIZE;
  int cy = y / CELL_SIZE;
  
  if (cx < 0 || cx >= COLS || cy < 0 || cy >= ROWS) return true;

  float lx = x - (cx * CELL_SIZE);
  float ly = y - (cy * CELL_SIZE);

  if ((maze[cx][cy] & WALL_TOP) && ly < playerRadius) return true;
  if ((maze[cx][cy] & WALL_BOTTOM) && ly > CELL_SIZE - playerRadius) return true;
  if ((maze[cx][cy] & WALL_LEFT) && lx < playerRadius) return true;
  if ((maze[cx][cy] & WALL_RIGHT) && lx > CELL_SIZE - playerRadius) return true;

  return false;
}

void handleInput() {
  bool currentBtnYState = digitalRead(BTN_Y);
  if (lastBtnYState == HIGH && currentBtnYState == LOW) showStatus = !showStatus; 
  lastBtnYState = currentBtnYState; 

  if (showStatus) {
    lastDrainTime = millis(); 
    return; 
  }

  if (digitalRead(BTN_LEFT) == LOW) playerAngle -= turnSpeed;
  if (digitalRead(BTN_RIGHT) == LOW) playerAngle += turnSpeed;

  float nextX = playerX;
  float nextY = playerY;

  if (digitalRead(BTN_UP) == LOW) {
    nextX += cos(playerAngle) * moveSpeed;
    nextY += sin(playerAngle) * moveSpeed;
  }
  if (digitalRead(BTN_DOWN) == LOW) {
    nextX -= cos(playerAngle) * moveSpeed;
    nextY -= sin(playerAngle) * moveSpeed;
  }

  if (!checkCollision(nextX, playerY)) playerX = nextX;
  if (!checkCollision(playerX, nextY)) playerY = nextY;

  bool currentBtnXState = digitalRead(BTN_X);
  if (lastBtnXState == HIGH && currentBtnXState == LOW) flashlightOn = !flashlightOn; 
  lastBtnXState = currentBtnXState; 
}

void updateGame() {
  int cx = playerX / CELL_SIZE;
  int cy = playerY / CELL_SIZE;
  
  if (cx == COLS - 1 && cy == 0) {
    generateMaze();
    playerX = CELL_SIZE / 2.0;
    playerY = SCREEN_HEIGHT - (CELL_SIZE / 2.0);
    playerAngle = -PI / 2.0; 
    
    float scavengeAmount = maxBattery - (score * ((maxBattery - 10.0) / 7.0));
    if (scavengeAmount < 10.0) scavengeAmount = 10.0;

    currentBattery += scavengeAmount; 
    if (currentBattery > maxBattery) currentBattery = maxBattery; 
    
    score++; 
    levelStartTime = millis(); 
    enemyActive = false;       
  }

  if (flashlightOn) {
    if (millis() - lastDrainTime > 150) { 
      currentBattery -= 0.15; 
      if (currentBattery <= 0.0) {
        currentBattery = 0.0; 
        gameState = GAMEOVER; 
      }
      lastDrainTime = millis();
    }
  } else {
    lastDrainTime = millis(); 
  }
}

// ==========================================
// --- ENEMY AI LOGIC ---
// ==========================================

void pickNextEnemyTarget() {
  int cx = enemyTargetCX;
  int cy = enemyTargetCY;
  
  int candidatesX[4], candidatesY[4], numCandidates = 0;
  
  if (!(maze[cx][cy] & WALL_TOP))    { candidatesX[numCandidates] = cx; candidatesY[numCandidates] = cy - 1; numCandidates++; }
  if (!(maze[cx][cy] & WALL_RIGHT))  { candidatesX[numCandidates] = cx + 1; candidatesY[numCandidates] = cy; numCandidates++; }
  if (!(maze[cx][cy] & WALL_BOTTOM)) { candidatesX[numCandidates] = cx; candidatesY[numCandidates] = cy + 1; numCandidates++; }
  if (!(maze[cx][cy] & WALL_LEFT))   { candidatesX[numCandidates] = cx - 1; candidatesY[numCandidates] = cy; numCandidates++; }
  
  if (numCandidates == 1) {
    enemyLastCX = cx; enemyLastCY = cy;
    enemyTargetCX = candidatesX[0]; enemyTargetCY = candidatesY[0];
    return;
  }
  
  int validX[4], validY[4], validCount = 0;
  for(int i = 0; i < numCandidates; i++) {
    if (candidatesX[i] != enemyLastCX || candidatesY[i] != enemyLastCY) {
      validX[validCount] = candidatesX[i]; validY[validCount] = candidatesY[i];
      validCount++;
    }
  }
  
  if (validCount > 0) {
    int choice = random(validCount);
    enemyLastCX = cx; enemyLastCY = cy;
    enemyTargetCX = validX[choice]; enemyTargetCY = validY[choice];
  }
}

void updateEnemy() {
  if (!enemyActive) {
    if (score >= 1 && (millis() - levelStartTime > 10000)) {
      enemyActive = true;
      enemyTargetCX = 0;         
      enemyTargetCY = ROWS - 1;  
      enemyLastCX = enemyTargetCX;
      enemyLastCY = enemyTargetCY;
      enemyX = enemyTargetCX * CELL_SIZE + CELL_SIZE / 2.0;
      enemyY = enemyTargetCY * CELL_SIZE + CELL_SIZE / 2.0;
      pickNextEnemyTarget();
    }
    return;
  }

  float enemySpeed = moveSpeed * min(1.25, 0.75 + (score * 0.05));
  float targetPixelX = enemyTargetCX * CELL_SIZE + CELL_SIZE / 2.0;
  float targetPixelY = enemyTargetCY * CELL_SIZE + CELL_SIZE / 2.0;
  
  float dx = targetPixelX - enemyX;
  float dy = targetPixelY - enemyY;
  float dist = sqrt(dx * dx + dy * dy);
  
  if (dist <= enemySpeed) {
    enemyX = targetPixelX;
    enemyY = targetPixelY;
    pickNextEnemyTarget();
  } else {
    enemyX += (dx / dist) * enemySpeed;
    enemyY += (dy / dist) * enemySpeed;
  }
  
  float pdx = playerX - enemyX;
  float pdy = playerY - enemyY;
  if (sqrt(pdx * pdx + pdy * pdy) < 7.0) gameState = GAMEOVER; 
}

// ==========================================
// --- RENDERING & MATH ---
// ==========================================

bool isPointInCone(float x, float y) {
  float dx = x - playerX;
  float dy = y - playerY;
  float distSq = dx * dx + dy * dy;

  if (distSq > currentBattery * currentBattery) return false; 
  if (distSq == 0) return true; 

  float dirX = cos(playerAngle);
  float dirY = sin(playerAngle);
  float dot = dx * dirX + dy * dirY;

  if (dot <= 0) return false; 

  float cosFovSq = 0.75; 
  return ((dot * dot) / distSq) >= cosFovSq;
}

bool isWallVisible(float x1, float y1, float x2, float y2) {
  if (!flashlightOn) return false;
  return isPointInCone(x1, y1) || isPointInCone(x2, y2) || isPointInCone((x1 + x2) / 2.0, (y1 + y2) / 2.0);
}

void drawGameOver() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(38, 20);
  display.print(F("GAME OVER"));
  display.setCursor(12, 40);
  display.print(F("Press Y to Restart"));
  display.display();
}

void handleGameOverInput() {
  if (digitalRead(BTN_Y) == LOW) {
    gameState = MENU; 
    delay(200); 
  }
}

void drawGame() {
  display.clearDisplay();

  if (showStatus) {
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    display.setCursor(30, 8);
    display.print(F("--- STATUS ---"));
    display.setCursor(15, 25);
    display.print(F("Mazes Beaten: "));
    display.print(score);
    
    int batPct = (int)((currentBattery / maxBattery) * 100);
    display.setCursor(10, 40);
    display.print(F("Power: "));
    display.print(batPct);
    display.print(F("%"));
    
    display.drawRect(76, 39, 42, 9, SH110X_WHITE);
    display.fillRect(78, 41, (int)(38.0 * (currentBattery / maxBattery)), 5, SH110X_WHITE);

    display.display();
    return; 
  }

  for (int x = 0; x < COLS; x++) {
    for (int y = 0; y < ROWS; y++) {
      int px = x * CELL_SIZE;
      int py = y * CELL_SIZE;
      
      if (maze[x][y] & WALL_TOP) {
        if (isWallVisible(px, py, px + CELL_SIZE, py)) display.drawLine(px, py, px + CELL_SIZE, py, SH110X_WHITE);
      }
      if (maze[x][y] & WALL_LEFT) {
        if (isWallVisible(px, py, px, py + CELL_SIZE)) display.drawLine(px, py, px, py + CELL_SIZE, SH110X_WHITE);
      }
      if (y == ROWS - 1 && (maze[x][y] & WALL_BOTTOM)) {
        if (isWallVisible(px, py + CELL_SIZE - 1, px + CELL_SIZE, py + CELL_SIZE - 1)) display.drawLine(px, py + CELL_SIZE - 1, px + CELL_SIZE, py + CELL_SIZE - 1, SH110X_WHITE);
      }
      if (x == COLS - 1 && (maze[x][y] & WALL_RIGHT)) {
        if (isWallVisible(px + CELL_SIZE - 1, py, px + CELL_SIZE - 1, py + CELL_SIZE)) display.drawLine(px + CELL_SIZE - 1, py, px + CELL_SIZE - 1, py + CELL_SIZE, SH110X_WHITE);
      }
    }
  }

  if ((millis() / 250) % 2 == 0) {
    display.fillRect(SCREEN_WIDTH - CELL_SIZE + 4, 4, CELL_SIZE - 8, CELL_SIZE - 8, SH110X_WHITE);
  }
  
  float s = sin(playerAngle);
  float c = cos(playerAngle);
  
  if (flashlightOn) {
    float angleLeft = playerAngle - (PI / 6.0);
    float angleRight = playerAngle + (PI / 6.0);
    
    int leftX = playerX + cos(angleLeft) * currentBattery;
    int leftY = playerY + sin(angleLeft) * currentBattery;
    
    int rightX = playerX + cos(angleRight) * currentBattery;
    int rightY = playerY + sin(angleRight) * currentBattery;

    display.drawLine((int)playerX, (int)playerY, leftX, leftY, SH110X_WHITE);
    display.drawLine((int)playerX, (int)playerY, rightX, rightY, SH110X_WHITE);
  }

  if (enemyActive) {
    bool enemyVisible = isPointInCone(enemyX, enemyY);
    if ((millis() % 3000) < 500) enemyVisible = true;
    if (enemyVisible) display.fillCircle((int)enemyX, (int)enemyY, 4, SH110X_WHITE);
  }

  for (int i = 0; i < 10; i++) {
    float px = iSprite[i][0];
    float py = iSprite[i][1];
    float rotatedX = (px * c) - (py * s);
    float rotatedY = (px * s) + (py * c);
    display.drawPixel((int)(playerX + rotatedX), (int)(playerY + rotatedY), SH110X_WHITE);
  }
  
  display.display();
}
Youtube
Tagi
glassmaze gra arduino labirynt projekt